From c0e8fb5085c6f137681616688808881530c9ec86 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 17 Apr 2019 16:44:26 +0400 Subject: [PATCH 001/211] p2p: (seed mode) limit the number of attempts to connect to a peer (#3573) * use dialPeer function in a seed mode Fixes #3532 by storing a number of attempts we've tried to connect in-memory and removing the address from addrbook when number of attempts > 16 --- CHANGELOG_PENDING.md | 2 + p2p/pex/pex_reactor.go | 79 +++++++++++++++++++++++++------------ p2p/pex/pex_reactor_test.go | 28 +++++++++++++ 3 files changed, 84 insertions(+), 25 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 36c64be5e..d628bc598 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,3 +20,5 @@ ### BUG FIXES: - [state] [\#3537](https://github.com/tendermint/tendermint/pull/3537#issuecomment-482711833) LoadValidators: do not return an empty validator set +- [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode + to 16 (as a result, the node will stop retrying after a 35 hours time window) diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index c24ee983c..38befe29a 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -48,6 +48,24 @@ const ( biasToSelectNewPeers = 30 // 70 to select good peers ) +type errMaxAttemptsToDial struct { +} + +func (e errMaxAttemptsToDial) Error() string { + return fmt.Sprintf("reached max attempts %d to dial", maxAttemptsToDial) +} + +type errTooEarlyToDial struct { + backoffDuration time.Duration + lastDialed time.Time +} + +func (e errTooEarlyToDial) Error() string { + return fmt.Sprintf( + "too early to dial (backoff duration: %d, last dialed: %v, time since: %v)", + e.backoffDuration, e.lastDialed, time.Since(e.lastDialed)) +} + // PEXReactor handles PEX (peer exchange) and ensures that an // adequate number of peers are connected to the switch. // @@ -449,7 +467,19 @@ func (r *PEXReactor) ensurePeers() { // Dial picked addresses for _, addr := range toDial { - go r.dialPeer(addr) + go func(addr *p2p.NetAddress) { + err := r.dialPeer(addr) + if err != nil { + switch err.(type) { + case errMaxAttemptsToDial: + r.Logger.Debug(err.Error(), "addr", addr) + case errTooEarlyToDial: + r.Logger.Debug(err.Error(), "addr", addr) + default: + r.Logger.Error(err.Error(), "addr", addr) + } + } + }(addr) } // If we need more addresses, pick a random peer and ask for more. @@ -479,17 +509,16 @@ func (r *PEXReactor) dialAttemptsInfo(addr *p2p.NetAddress) (attempts int, lastD return atd.number, atd.lastDialed } -func (r *PEXReactor) dialPeer(addr *p2p.NetAddress) { +func (r *PEXReactor) dialPeer(addr *p2p.NetAddress) error { attempts, lastDialed := r.dialAttemptsInfo(addr) if attempts > maxAttemptsToDial { - // Do not log the message if the addr gets readded. - if attempts+1 == maxAttemptsToDial { - r.Logger.Info("Reached max attempts to dial", "addr", addr, "attempts", attempts) - r.attemptsToDial.Store(addr.DialString(), _attemptsToDial{attempts + 1, time.Now()}) - } + // TODO(melekes): have a blacklist in the addrbook with peers whom we've + // failed to connect to. Then we can clean up attemptsToDial, which acts as + // a blacklist currently. + // https://github.com/tendermint/tendermint/issues/3572 r.book.MarkBad(addr) - return + return errMaxAttemptsToDial{} } // exponential backoff if it's not our first attempt to dial given address @@ -498,33 +527,31 @@ func (r *PEXReactor) dialPeer(addr *p2p.NetAddress) { backoffDuration := jitterSeconds + ((1 << uint(attempts)) * time.Second) sinceLastDialed := time.Since(lastDialed) if sinceLastDialed < backoffDuration { - r.Logger.Debug("Too early to dial", "addr", addr, "backoff_duration", backoffDuration, "last_dialed", lastDialed, "time_since", sinceLastDialed) - return + return errTooEarlyToDial{backoffDuration, lastDialed} } } err := r.Switch.DialPeerWithAddress(addr, false) + if err != nil { if _, ok := err.(p2p.ErrCurrentlyDialingOrExistingAddress); ok { - return + return err } - r.Logger.Error("Dialing failed", "addr", addr, "err", err, "attempts", attempts) markAddrInBookBasedOnErr(addr, r.book, err) - if _, ok := err.(p2p.ErrSwitchAuthenticationFailure); ok { + switch err.(type) { + case p2p.ErrSwitchAuthenticationFailure: + // NOTE: addr is removed from addrbook in markAddrInBookBasedOnErr r.attemptsToDial.Delete(addr.DialString()) - } else { - // FIXME: if the addr is going to be removed from the addrbook (hard to - // tell at this point), we need to Delete it from attemptsToDial, not - // record another attempt. - // record attempt + default: r.attemptsToDial.Store(addr.DialString(), _attemptsToDial{attempts + 1, time.Now()}) } - return + return errors.Wrapf(err, "dialing failed (attempts: %d)", attempts+1) } // cleanup any history r.attemptsToDial.Delete(addr.DialString()) + return nil } // checkSeeds checks that addresses are well formed. @@ -633,14 +660,16 @@ func (r *PEXReactor) crawlPeers(addrs []*p2p.NetAddress) { LastCrawled: now, } - err := r.Switch.DialPeerWithAddress(addr, false) + err := r.dialPeer(addr) if err != nil { - if _, ok := err.(p2p.ErrCurrentlyDialingOrExistingAddress); ok { - continue + switch err.(type) { + case errMaxAttemptsToDial: + r.Logger.Debug(err.Error(), "addr", addr) + case errTooEarlyToDial: + r.Logger.Debug(err.Error(), "addr", addr) + default: + r.Logger.Error(err.Error(), "addr", addr) } - - r.Logger.Error("Dialing failed", "addr", addr, "err", err) - markAddrInBookBasedOnErr(addr, r.book, err) continue } diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 077f07a60..6572a5f61 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -322,6 +322,34 @@ func TestPEXReactorSeedMode(t *testing.T) { assert.Equal(t, 0, sw.Peers().Size()) } +func TestPEXReactorDialsPeerUpToMaxAttemptsInSeedMode(t *testing.T) { + // directory to store address books + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(t, err) + defer os.RemoveAll(dir) // nolint: errcheck + + pexR, book := createReactor(&PEXReactorConfig{SeedMode: true}) + defer teardownReactor(book) + + sw := createSwitchAndAddReactors(pexR) + sw.SetAddrBook(book) + err = sw.Start() + require.NoError(t, err) + defer sw.Stop() + + peer := mock.NewPeer(nil) + addr := peer.SocketAddr() + + err = book.AddAddress(addr, addr) + require.NoError(t, err) + + assert.True(t, book.HasAddress(addr)) + // imitate maxAttemptsToDial reached + pexR.attemptsToDial.Store(addr.DialString(), _attemptsToDial{maxAttemptsToDial + 1, time.Now()}) + pexR.crawlPeers([]*p2p.NetAddress{addr}) + assert.False(t, book.HasAddress(addr)) +} + // connect a peer to a seed, wait a bit, then stop it. // this should give it time to request addrs and for the seed // to call FlushStop, and allows us to test calling Stop concurrently From 621c0e629d3aa80ac45ffc92c20709bea6702b32 Mon Sep 17 00:00:00 2001 From: kevlubkcm <36485490+kevlubkcm@users.noreply.github.com> Date: Wed, 17 Apr 2019 11:09:17 -0400 Subject: [PATCH 002/211] docs: fix typo in clist readme (#3574) --- libs/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/README.md b/libs/README.md index 9ea618dbd..b1bb0396c 100644 --- a/libs/README.md +++ b/libs/README.md @@ -13,7 +13,7 @@ CLI wraps the `cobra` and `viper` packages and handles some common elements of b ## clist -Clist provides a linekd list that is safe for concurrent access by many readers. +Clist provides a linked list that is safe for concurrent access by many readers. ## common From 90465f727f122367f4bf1f808e3e352fd1b5d11c Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Wed, 17 Apr 2019 11:10:12 -0400 Subject: [PATCH 003/211] rpc: add support for batched requests/responses (#3534) Continues from #3280 in building support for batched requests/responses in the JSON RPC (as per issue #3213). * Add JSON RPC batching for client and server As per #3213, this adds support for [JSON RPC batch requests and responses](https://www.jsonrpc.org/specification#batch). * Add additional checks to ensure client responses are the same as results * Fix case where a notification is sent and no response is expected * Add test to check that JSON RPC notifications in a batch are left out in responses * Update CHANGELOG_PENDING.md * Update PR number now that PR has been created * Make errors start with lowercase letter * Refactor batch functionality to be standalone This refactors the batching functionality to rather act in a standalone way. In light of supporting concurrent goroutines making use of the same client, it would make sense to have batching functionality where one could create a batch of requests per goroutine and send that batch without interfering with a batch from another goroutine. * Add examples for simple and batch HTTP client usage * Check errors from writer and remove nolinter directives * Make error strings start with lowercase letter * Refactor examples to make them testable * Use safer deferred shutdown for example Tendermint test node * Recompose rpcClient interface from pre-existing interface components * Rename WaitGroup for brevity * Replace empty ID string with request ID * Remove extraneous test case * Convert first letter of errors.Wrap() messages to lowercase * Remove extraneous function parameter * Make variable declaration terse * Reorder WaitGroup.Done call to help prevent race conditions in the face of failure * Swap mutex to value representation and remove initialization * Restore empty JSONRPC string ID in response to prevent nil * Make JSONRPCBufferedRequest private * Revert PR hard link in CHANGELOG_PENDING * Add client ID for JSONRPCClient This adds code to automatically generate a randomized client ID for the JSONRPCClient, and adds a check of the IDs in the responses (if one was set in the requests). * Extract response ID validation into separate function * Remove extraneous comments * Reorder fields to indicate clearly which are protected by the mutex * Refactor for loop to remove indexing * Restructure and combine loop * Flatten conditional block for better readability * Make multi-variable declaration slightly more readable * Change for loop style * Compress error check statements * Make function description more generic to show that we support different protocols * Preallocate memory for request and result objects --- CHANGELOG_PENDING.md | 1 + rpc/client/examples_test.go | 126 +++++++++++++++++++++ rpc/client/helpers.go | 2 +- rpc/client/httpclient.go | 187 +++++++++++++++++++++++--------- rpc/client/rpc_test.go | 100 +++++++++++++++++ rpc/lib/client/http_client.go | 186 +++++++++++++++++++++++++++++-- rpc/lib/server/handlers.go | 98 +++++++++-------- rpc/lib/server/handlers_test.go | 66 +++++++++++ rpc/lib/server/http_params.go | 2 +- rpc/lib/server/http_server.go | 33 +++++- rpc/test/helpers.go | 77 +++++++++---- 11 files changed, 746 insertions(+), 132 deletions(-) create mode 100644 rpc/client/examples_test.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d628bc598..3f151f71a 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -17,6 +17,7 @@ ### FEATURES: ### IMPROVEMENTS: +- [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC ### BUG FIXES: - [state] [\#3537](https://github.com/tendermint/tendermint/pull/3537#issuecomment-482711833) LoadValidators: do not return an empty validator set diff --git a/rpc/client/examples_test.go b/rpc/client/examples_test.go new file mode 100644 index 000000000..720e48492 --- /dev/null +++ b/rpc/client/examples_test.go @@ -0,0 +1,126 @@ +package client_test + +import ( + "bytes" + "fmt" + + "github.com/tendermint/tendermint/abci/example/kvstore" + "github.com/tendermint/tendermint/rpc/client" + ctypes "github.com/tendermint/tendermint/rpc/core/types" + rpctest "github.com/tendermint/tendermint/rpc/test" +) + +func ExampleHTTP_simple() { + // Start a tendermint node (and kvstore) in the background to test against + app := kvstore.NewKVStoreApplication() + node := rpctest.StartTendermint(app, rpctest.SuppressStdout, rpctest.RecreateConfig) + defer rpctest.StopTendermint(node) + + // Create our RPC client + rpcAddr := rpctest.GetConfig().RPC.ListenAddress + c := client.NewHTTP(rpcAddr, "/websocket") + + // Create a transaction + k := []byte("name") + v := []byte("satoshi") + tx := append(k, append([]byte("="), v...)...) + + // Broadcast the transaction and wait for it to commit (rather use + // c.BroadcastTxSync though in production) + bres, err := c.BroadcastTxCommit(tx) + if err != nil { + panic(err) + } + if bres.CheckTx.IsErr() || bres.DeliverTx.IsErr() { + panic("BroadcastTxCommit transaction failed") + } + + // Now try to fetch the value for the key + qres, err := c.ABCIQuery("/key", k) + if err != nil { + panic(err) + } + if qres.Response.IsErr() { + panic("ABCIQuery failed") + } + if !bytes.Equal(qres.Response.Key, k) { + panic("returned key does not match queried key") + } + if !bytes.Equal(qres.Response.Value, v) { + panic("returned value does not match sent value") + } + + fmt.Println("Sent tx :", string(tx)) + fmt.Println("Queried for :", string(qres.Response.Key)) + fmt.Println("Got value :", string(qres.Response.Value)) + + // Output: + // Sent tx : name=satoshi + // Queried for : name + // Got value : satoshi +} + +func ExampleHTTP_batching() { + // Start a tendermint node (and kvstore) in the background to test against + app := kvstore.NewKVStoreApplication() + node := rpctest.StartTendermint(app, rpctest.SuppressStdout, rpctest.RecreateConfig) + defer rpctest.StopTendermint(node) + + // Create our RPC client + rpcAddr := rpctest.GetConfig().RPC.ListenAddress + c := client.NewHTTP(rpcAddr, "/websocket") + + // Create our two transactions + k1 := []byte("firstName") + v1 := []byte("satoshi") + tx1 := append(k1, append([]byte("="), v1...)...) + + k2 := []byte("lastName") + v2 := []byte("nakamoto") + tx2 := append(k2, append([]byte("="), v2...)...) + + txs := [][]byte{tx1, tx2} + + // Create a new batch + batch := c.NewBatch() + + // Queue up our transactions + for _, tx := range txs { + if _, err := batch.BroadcastTxCommit(tx); err != nil { + panic(err) + } + } + + // Send the batch of 2 transactions + if _, err := batch.Send(); err != nil { + panic(err) + } + + // Now let's query for the original results as a batch + keys := [][]byte{k1, k2} + for _, key := range keys { + if _, err := batch.ABCIQuery("/key", key); err != nil { + panic(err) + } + } + + // Send the 2 queries and keep the results + results, err := batch.Send() + if err != nil { + panic(err) + } + + // Each result in the returned list is the deserialized result of each + // respective ABCIQuery response + for _, result := range results { + qr, ok := result.(*ctypes.ResultABCIQuery) + if !ok { + panic("invalid result type from ABCIQuery request") + } + fmt.Println(string(qr.Response.Key), "=", string(qr.Response.Value)) + } + + // Output: + // firstName = satoshi + // lastName = nakamoto +} diff --git a/rpc/client/helpers.go b/rpc/client/helpers.go index 4889b0740..756ba2818 100644 --- a/rpc/client/helpers.go +++ b/rpc/client/helpers.go @@ -15,7 +15,7 @@ type Waiter func(delta int64) (abort error) // but you can plug in another one func DefaultWaitStrategy(delta int64) (abort error) { if delta > 10 { - return errors.Errorf("Waiting for %d blocks... aborting", delta) + return errors.Errorf("waiting for %d blocks... aborting", delta) } else if delta > 0 { // estimate of wait time.... // wait half a second for the next block (in progress) diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index 55c7b4f17..3fd13da37 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -18,27 +18,72 @@ import ( ) /* -HTTP is a Client implementation that communicates with a tendermint node over -json rpc and websockets. +HTTP is a Client implementation that communicates with a Tendermint node over +JSON RPC and WebSockets. This is the main implementation you probably want to use in production code. -There are other implementations when calling the tendermint node in-process +There are other implementations when calling the Tendermint node in-process (Local), or when you want to mock out the server for test code (mock). You can subscribe for any event published by Tendermint using Subscribe method. -Note delivery is best-effort. If you don't read events fast enough or network -is slow, Tendermint might cancel the subscription. The client will attempt to +Note delivery is best-effort. If you don't read events fast enough or network is +slow, Tendermint might cancel the subscription. The client will attempt to resubscribe (you don't need to do anything). It will keep trying every second indefinitely until successful. + +Request batching is available for JSON RPC requests over HTTP, which conforms to +the JSON RPC specification (https://www.jsonrpc.org/specification#batch). See +the example for more details. */ type HTTP struct { remote string rpc *rpcclient.JSONRPCClient + + *baseRPCClient *WSEvents } -// NewHTTP takes a remote endpoint in the form tcp://: -// and the websocket path (which always seems to be "/websocket") +// BatchHTTP provides the same interface as `HTTP`, but allows for batching of +// requests (as per https://www.jsonrpc.org/specification#batch). Do not +// instantiate directly - rather use the HTTP.NewBatch() method to create an +// instance of this struct. +// +// Batching of HTTP requests is thread-safe in the sense that multiple +// goroutines can each create their own batches and send them using the same +// HTTP client. Multiple goroutines could also enqueue transactions in a single +// batch, but ordering of transactions in the batch cannot be guaranteed in such +// an example. +type BatchHTTP struct { + rpcBatch *rpcclient.JSONRPCRequestBatch + *baseRPCClient +} + +// rpcClient is an internal interface to which our RPC clients (batch and +// non-batch) must conform. Acts as an additional code-level sanity check to +// make sure the implementations stay coherent. +type rpcClient interface { + ABCIClient + HistoryClient + NetworkClient + SignClient + StatusClient +} + +// baseRPCClient implements the basic RPC method logic without the actual +// underlying RPC call functionality, which is provided by `caller`. +type baseRPCClient struct { + caller rpcclient.JSONRPCCaller +} + +var _ rpcClient = (*HTTP)(nil) +var _ rpcClient = (*BatchHTTP)(nil) +var _ rpcClient = (*baseRPCClient)(nil) + +//----------------------------------------------------------------------------- +// HTTP + +// NewHTTP takes a remote endpoint in the form ://: and +// the websocket path (which always seems to be "/websocket") func NewHTTP(remote, wsEndpoint string) *HTTP { rc := rpcclient.NewJSONRPCClient(remote) cdc := rc.Codec() @@ -46,39 +91,76 @@ func NewHTTP(remote, wsEndpoint string) *HTTP { rc.SetCodec(cdc) return &HTTP{ - rpc: rc, - remote: remote, - WSEvents: newWSEvents(cdc, remote, wsEndpoint), + rpc: rc, + remote: remote, + baseRPCClient: &baseRPCClient{caller: rc}, + WSEvents: newWSEvents(cdc, remote, wsEndpoint), } } var _ Client = (*HTTP)(nil) -func (c *HTTP) Status() (*ctypes.ResultStatus, error) { +// NewBatch creates a new batch client for this HTTP client. +func (c *HTTP) NewBatch() *BatchHTTP { + rpcBatch := c.rpc.NewRequestBatch() + return &BatchHTTP{ + rpcBatch: rpcBatch, + baseRPCClient: &baseRPCClient{ + caller: rpcBatch, + }, + } +} + +//----------------------------------------------------------------------------- +// BatchHTTP + +// Send is a convenience function for an HTTP batch that will trigger the +// compilation of the batched requests and send them off using the client as a +// single request. On success, this returns a list of the deserialized results +// from each request in the sent batch. +func (b *BatchHTTP) Send() ([]interface{}, error) { + return b.rpcBatch.Send() +} + +// Clear will empty out this batch of requests and return the number of requests +// that were cleared out. +func (b *BatchHTTP) Clear() int { + return b.rpcBatch.Clear() +} + +// Count returns the number of enqueued requests waiting to be sent. +func (b *BatchHTTP) Count() int { + return b.rpcBatch.Count() +} + +//----------------------------------------------------------------------------- +// baseRPCClient + +func (c *baseRPCClient) Status() (*ctypes.ResultStatus, error) { result := new(ctypes.ResultStatus) - _, err := c.rpc.Call("status", map[string]interface{}{}, result) + _, err := c.caller.Call("status", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "Status") } return result, nil } -func (c *HTTP) ABCIInfo() (*ctypes.ResultABCIInfo, error) { +func (c *baseRPCClient) ABCIInfo() (*ctypes.ResultABCIInfo, error) { result := new(ctypes.ResultABCIInfo) - _, err := c.rpc.Call("abci_info", map[string]interface{}{}, result) + _, err := c.caller.Call("abci_info", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "ABCIInfo") } return result, nil } -func (c *HTTP) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { +func (c *baseRPCClient) ABCIQuery(path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { return c.ABCIQueryWithOptions(path, data, DefaultABCIQueryOptions) } -func (c *HTTP) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { +func (c *baseRPCClient) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { result := new(ctypes.ResultABCIQuery) - _, err := c.rpc.Call("abci_query", + _, err := c.caller.Call("abci_query", map[string]interface{}{"path": path, "data": data, "height": opts.Height, "prove": opts.Prove}, result) if err != nil { @@ -87,89 +169,89 @@ func (c *HTTP) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts ABCIQue return result, nil } -func (c *HTTP) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { +func (c *baseRPCClient) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { result := new(ctypes.ResultBroadcastTxCommit) - _, err := c.rpc.Call("broadcast_tx_commit", map[string]interface{}{"tx": tx}, result) + _, err := c.caller.Call("broadcast_tx_commit", map[string]interface{}{"tx": tx}, result) if err != nil { return nil, errors.Wrap(err, "broadcast_tx_commit") } return result, nil } -func (c *HTTP) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { +func (c *baseRPCClient) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { return c.broadcastTX("broadcast_tx_async", tx) } -func (c *HTTP) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { +func (c *baseRPCClient) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { return c.broadcastTX("broadcast_tx_sync", tx) } -func (c *HTTP) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { +func (c *baseRPCClient) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { result := new(ctypes.ResultBroadcastTx) - _, err := c.rpc.Call(route, map[string]interface{}{"tx": tx}, result) + _, err := c.caller.Call(route, map[string]interface{}{"tx": tx}, result) if err != nil { return nil, errors.Wrap(err, route) } return result, nil } -func (c *HTTP) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) { +func (c *baseRPCClient) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) { result := new(ctypes.ResultUnconfirmedTxs) - _, err := c.rpc.Call("unconfirmed_txs", map[string]interface{}{"limit": limit}, result) + _, err := c.caller.Call("unconfirmed_txs", map[string]interface{}{"limit": limit}, result) if err != nil { return nil, errors.Wrap(err, "unconfirmed_txs") } return result, nil } -func (c *HTTP) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { +func (c *baseRPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { result := new(ctypes.ResultUnconfirmedTxs) - _, err := c.rpc.Call("num_unconfirmed_txs", map[string]interface{}{}, result) + _, err := c.caller.Call("num_unconfirmed_txs", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "num_unconfirmed_txs") } return result, nil } -func (c *HTTP) NetInfo() (*ctypes.ResultNetInfo, error) { +func (c *baseRPCClient) NetInfo() (*ctypes.ResultNetInfo, error) { result := new(ctypes.ResultNetInfo) - _, err := c.rpc.Call("net_info", map[string]interface{}{}, result) + _, err := c.caller.Call("net_info", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "NetInfo") } return result, nil } -func (c *HTTP) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { +func (c *baseRPCClient) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { result := new(ctypes.ResultDumpConsensusState) - _, err := c.rpc.Call("dump_consensus_state", map[string]interface{}{}, result) + _, err := c.caller.Call("dump_consensus_state", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "DumpConsensusState") } return result, nil } -func (c *HTTP) ConsensusState() (*ctypes.ResultConsensusState, error) { +func (c *baseRPCClient) ConsensusState() (*ctypes.ResultConsensusState, error) { result := new(ctypes.ResultConsensusState) - _, err := c.rpc.Call("consensus_state", map[string]interface{}{}, result) + _, err := c.caller.Call("consensus_state", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "ConsensusState") } return result, nil } -func (c *HTTP) Health() (*ctypes.ResultHealth, error) { +func (c *baseRPCClient) Health() (*ctypes.ResultHealth, error) { result := new(ctypes.ResultHealth) - _, err := c.rpc.Call("health", map[string]interface{}{}, result) + _, err := c.caller.Call("health", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "Health") } return result, nil } -func (c *HTTP) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { +func (c *baseRPCClient) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { result := new(ctypes.ResultBlockchainInfo) - _, err := c.rpc.Call("blockchain", + _, err := c.caller.Call("blockchain", map[string]interface{}{"minHeight": minHeight, "maxHeight": maxHeight}, result) if err != nil { @@ -178,56 +260,56 @@ func (c *HTTP) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockch return result, nil } -func (c *HTTP) Genesis() (*ctypes.ResultGenesis, error) { +func (c *baseRPCClient) Genesis() (*ctypes.ResultGenesis, error) { result := new(ctypes.ResultGenesis) - _, err := c.rpc.Call("genesis", map[string]interface{}{}, result) + _, err := c.caller.Call("genesis", map[string]interface{}{}, result) if err != nil { return nil, errors.Wrap(err, "Genesis") } return result, nil } -func (c *HTTP) Block(height *int64) (*ctypes.ResultBlock, error) { +func (c *baseRPCClient) Block(height *int64) (*ctypes.ResultBlock, error) { result := new(ctypes.ResultBlock) - _, err := c.rpc.Call("block", map[string]interface{}{"height": height}, result) + _, err := c.caller.Call("block", map[string]interface{}{"height": height}, result) if err != nil { return nil, errors.Wrap(err, "Block") } return result, nil } -func (c *HTTP) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { +func (c *baseRPCClient) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { result := new(ctypes.ResultBlockResults) - _, err := c.rpc.Call("block_results", map[string]interface{}{"height": height}, result) + _, err := c.caller.Call("block_results", map[string]interface{}{"height": height}, result) if err != nil { return nil, errors.Wrap(err, "Block Result") } return result, nil } -func (c *HTTP) Commit(height *int64) (*ctypes.ResultCommit, error) { +func (c *baseRPCClient) Commit(height *int64) (*ctypes.ResultCommit, error) { result := new(ctypes.ResultCommit) - _, err := c.rpc.Call("commit", map[string]interface{}{"height": height}, result) + _, err := c.caller.Call("commit", map[string]interface{}{"height": height}, result) if err != nil { return nil, errors.Wrap(err, "Commit") } return result, nil } -func (c *HTTP) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { +func (c *baseRPCClient) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { result := new(ctypes.ResultTx) params := map[string]interface{}{ "hash": hash, "prove": prove, } - _, err := c.rpc.Call("tx", params, result) + _, err := c.caller.Call("tx", params, result) if err != nil { return nil, errors.Wrap(err, "Tx") } return result, nil } -func (c *HTTP) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { +func (c *baseRPCClient) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { result := new(ctypes.ResultTxSearch) params := map[string]interface{}{ "query": query, @@ -235,23 +317,24 @@ func (c *HTTP) TxSearch(query string, prove bool, page, perPage int) (*ctypes.Re "page": page, "per_page": perPage, } - _, err := c.rpc.Call("tx_search", params, result) + _, err := c.caller.Call("tx_search", params, result) if err != nil { return nil, errors.Wrap(err, "TxSearch") } return result, nil } -func (c *HTTP) Validators(height *int64) (*ctypes.ResultValidators, error) { +func (c *baseRPCClient) Validators(height *int64) (*ctypes.ResultValidators, error) { result := new(ctypes.ResultValidators) - _, err := c.rpc.Call("validators", map[string]interface{}{"height": height}, result) + _, err := c.caller.Call("validators", map[string]interface{}{"height": height}, result) if err != nil { return nil, errors.Wrap(err, "Validators") } return result, nil } -/** websocket event stuff here... **/ +//----------------------------------------------------------------------------- +// WSEvents type WSEvents struct { cmn.BaseService diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index ba9bc3af7..1544a3d95 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "strings" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -11,7 +12,9 @@ import ( abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/rpc/client" + ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctest "github.com/tendermint/tendermint/rpc/test" "github.com/tendermint/tendermint/types" ) @@ -441,3 +444,100 @@ func TestTxSearch(t *testing.T) { require.Len(t, result.Txs, 0) } } + +func TestBatchedJSONRPCCalls(t *testing.T) { + c := getHTTPClient() + testBatchedJSONRPCCalls(t, c) +} + +func testBatchedJSONRPCCalls(t *testing.T, c *client.HTTP) { + k1, v1, tx1 := MakeTxKV() + k2, v2, tx2 := MakeTxKV() + + batch := c.NewBatch() + r1, err := batch.BroadcastTxCommit(tx1) + require.NoError(t, err) + r2, err := batch.BroadcastTxCommit(tx2) + require.NoError(t, err) + require.Equal(t, 2, batch.Count()) + bresults, err := batch.Send() + require.NoError(t, err) + require.Len(t, bresults, 2) + require.Equal(t, 0, batch.Count()) + + bresult1, ok := bresults[0].(*ctypes.ResultBroadcastTxCommit) + require.True(t, ok) + require.Equal(t, *bresult1, *r1) + bresult2, ok := bresults[1].(*ctypes.ResultBroadcastTxCommit) + require.True(t, ok) + require.Equal(t, *bresult2, *r2) + apph := cmn.MaxInt64(bresult1.Height, bresult2.Height) + 1 + + client.WaitForHeight(c, apph, nil) + + q1, err := batch.ABCIQuery("/key", k1) + require.NoError(t, err) + q2, err := batch.ABCIQuery("/key", k2) + require.NoError(t, err) + require.Equal(t, 2, batch.Count()) + qresults, err := batch.Send() + require.NoError(t, err) + require.Len(t, qresults, 2) + require.Equal(t, 0, batch.Count()) + + qresult1, ok := qresults[0].(*ctypes.ResultABCIQuery) + require.True(t, ok) + require.Equal(t, *qresult1, *q1) + qresult2, ok := qresults[1].(*ctypes.ResultABCIQuery) + require.True(t, ok) + require.Equal(t, *qresult2, *q2) + + require.Equal(t, qresult1.Response.Key, k1) + require.Equal(t, qresult2.Response.Key, k2) + require.Equal(t, qresult1.Response.Value, v1) + require.Equal(t, qresult2.Response.Value, v2) +} + +func TestBatchedJSONRPCCallsCancellation(t *testing.T) { + c := getHTTPClient() + _, _, tx1 := MakeTxKV() + _, _, tx2 := MakeTxKV() + + batch := c.NewBatch() + _, err := batch.BroadcastTxCommit(tx1) + require.NoError(t, err) + _, err = batch.BroadcastTxCommit(tx2) + require.NoError(t, err) + // we should have 2 requests waiting + require.Equal(t, 2, batch.Count()) + // we want to make sure we cleared 2 pending requests + require.Equal(t, 2, batch.Clear()) + // now there should be no batched requests + require.Equal(t, 0, batch.Count()) +} + +func TestSendingEmptyJSONRPCRequestBatch(t *testing.T) { + c := getHTTPClient() + batch := c.NewBatch() + _, err := batch.Send() + require.Error(t, err, "sending an empty batch of JSON RPC requests should result in an error") +} + +func TestClearingEmptyJSONRPCRequestBatch(t *testing.T) { + c := getHTTPClient() + batch := c.NewBatch() + require.Zero(t, batch.Clear(), "clearing an empty batch of JSON RPC requests should result in a 0 result") +} + +func TestConcurrentJSONRPCBatching(t *testing.T) { + var wg sync.WaitGroup + c := getHTTPClient() + for i := 0; i < 50; i++ { + wg.Add(1) + go func() { + defer wg.Done() + testBatchedJSONRPCCalls(t, c) + }() + } + wg.Wait() +} diff --git a/rpc/lib/client/http_client.go b/rpc/lib/client/http_client.go index cfa26e89c..824820fab 100644 --- a/rpc/lib/client/http_client.go +++ b/rpc/lib/client/http_client.go @@ -10,9 +10,11 @@ import ( "net/url" "reflect" "strings" + "sync" "github.com/pkg/errors" amino "github.com/tendermint/go-amino" + cmn "github.com/tendermint/tendermint/libs/common" types "github.com/tendermint/tendermint/rpc/lib/types" ) @@ -83,25 +85,56 @@ func makeHTTPClient(remoteAddr string) (string, *http.Client) { //------------------------------------------------------------------------------------ +// jsonRPCBufferedRequest encapsulates a single buffered request, as well as its +// anticipated response structure. +type jsonRPCBufferedRequest struct { + request types.RPCRequest + result interface{} // The result will be deserialized into this object. +} + +// JSONRPCRequestBatch allows us to buffer multiple request/response structures +// into a single batch request. Note that this batch acts like a FIFO queue, and +// is thread-safe. +type JSONRPCRequestBatch struct { + client *JSONRPCClient + + mtx sync.Mutex + requests []*jsonRPCBufferedRequest +} + // JSONRPCClient takes params as a slice type JSONRPCClient struct { address string client *http.Client + id types.JSONRPCStringID cdc *amino.Codec } +// JSONRPCCaller implementers can facilitate calling the JSON RPC endpoint. +type JSONRPCCaller interface { + Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) +} + +// Both JSONRPCClient and JSONRPCRequestBatch can facilitate calls to the JSON +// RPC endpoint. +var _ JSONRPCCaller = (*JSONRPCClient)(nil) +var _ JSONRPCCaller = (*JSONRPCRequestBatch)(nil) + // NewJSONRPCClient returns a JSONRPCClient pointed at the given address. func NewJSONRPCClient(remote string) *JSONRPCClient { address, client := makeHTTPClient(remote) return &JSONRPCClient{ address: address, client: client, + id: types.JSONRPCStringID("jsonrpc-client-" + cmn.RandStr(8)), cdc: amino.NewCodec(), } } +// Call will send the request for the given method through to the RPC endpoint +// immediately, without buffering of requests. func (c *JSONRPCClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - request, err := types.MapToRequest(c.cdc, types.JSONRPCStringID("jsonrpc-client"), method, params) + request, err := types.MapToRequest(c.cdc, c.id, method, params) if err != nil { return nil, err } @@ -109,9 +142,7 @@ func (c *JSONRPCClient) Call(method string, params map[string]interface{}, resul if err != nil { return nil, err } - // log.Info(string(requestBytes)) requestBuf := bytes.NewBuffer(requestBytes) - // log.Info(Fmt("RPC request to %v (%v): %v", c.remote, method, string(requestBytes))) httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) if err != nil { return nil, err @@ -122,8 +153,40 @@ func (c *JSONRPCClient) Call(method string, params map[string]interface{}, resul if err != nil { return nil, err } - // log.Info(Fmt("RPC response: %v", string(responseBytes))) - return unmarshalResponseBytes(c.cdc, responseBytes, result) + return unmarshalResponseBytes(c.cdc, responseBytes, c.id, result) +} + +// NewRequestBatch starts a batch of requests for this client. +func (c *JSONRPCClient) NewRequestBatch() *JSONRPCRequestBatch { + return &JSONRPCRequestBatch{ + requests: make([]*jsonRPCBufferedRequest, 0), + client: c, + } +} + +func (c *JSONRPCClient) sendBatch(requests []*jsonRPCBufferedRequest) ([]interface{}, error) { + reqs := make([]types.RPCRequest, 0, len(requests)) + results := make([]interface{}, 0, len(requests)) + for _, req := range requests { + reqs = append(reqs, req.request) + results = append(results, req.result) + } + // serialize the array of requests into a single JSON object + requestBytes, err := json.Marshal(reqs) + if err != nil { + return nil, err + } + httpResponse, err := c.client.Post(c.address, "text/json", bytes.NewBuffer(requestBytes)) + if err != nil { + return nil, err + } + defer httpResponse.Body.Close() // nolint: errcheck + + responseBytes, err := ioutil.ReadAll(httpResponse.Body) + if err != nil { + return nil, err + } + return unmarshalResponseBytesArray(c.cdc, responseBytes, c.id, results) } func (c *JSONRPCClient) Codec() *amino.Codec { @@ -136,6 +199,57 @@ func (c *JSONRPCClient) SetCodec(cdc *amino.Codec) { //------------------------------------------------------------- +// Count returns the number of enqueued requests waiting to be sent. +func (b *JSONRPCRequestBatch) Count() int { + b.mtx.Lock() + defer b.mtx.Unlock() + return len(b.requests) +} + +func (b *JSONRPCRequestBatch) enqueue(req *jsonRPCBufferedRequest) { + b.mtx.Lock() + defer b.mtx.Unlock() + b.requests = append(b.requests, req) +} + +// Clear empties out the request batch. +func (b *JSONRPCRequestBatch) Clear() int { + b.mtx.Lock() + defer b.mtx.Unlock() + return b.clear() +} + +func (b *JSONRPCRequestBatch) clear() int { + count := len(b.requests) + b.requests = make([]*jsonRPCBufferedRequest, 0) + return count +} + +// Send will attempt to send the current batch of enqueued requests, and then +// will clear out the requests once done. On success, this returns the +// deserialized list of results from each of the enqueued requests. +func (b *JSONRPCRequestBatch) Send() ([]interface{}, error) { + b.mtx.Lock() + defer func() { + b.clear() + b.mtx.Unlock() + }() + return b.client.sendBatch(b.requests) +} + +// Call enqueues a request to call the given RPC method with the specified +// parameters, in the same way that the `JSONRPCClient.Call` function would. +func (b *JSONRPCRequestBatch) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { + request, err := types.MapToRequest(b.client.cdc, b.client.id, method, params) + if err != nil { + return nil, err + } + b.enqueue(&jsonRPCBufferedRequest{request: request, result: result}) + return result, nil +} + +//------------------------------------------------------------- + // URI takes params as a map type URIClient struct { address string @@ -168,7 +282,7 @@ func (c *URIClient) Call(method string, params map[string]interface{}, result in if err != nil { return nil, err } - return unmarshalResponseBytes(c.cdc, responseBytes, result) + return unmarshalResponseBytes(c.cdc, responseBytes, "", result) } func (c *URIClient) Codec() *amino.Codec { @@ -181,7 +295,7 @@ func (c *URIClient) SetCodec(cdc *amino.Codec) { //------------------------------------------------ -func unmarshalResponseBytes(cdc *amino.Codec, responseBytes []byte, result interface{}) (interface{}, error) { +func unmarshalResponseBytes(cdc *amino.Codec, responseBytes []byte, expectedID types.JSONRPCStringID, result interface{}) (interface{}, error) { // Read response. If rpc/core/types is imported, the result will unmarshal // into the correct type. // log.Notice("response", "response", string(responseBytes)) @@ -189,19 +303,71 @@ func unmarshalResponseBytes(cdc *amino.Codec, responseBytes []byte, result inter response := &types.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { - return nil, errors.Errorf("Error unmarshalling rpc response: %v", err) + return nil, errors.Errorf("error unmarshalling rpc response: %v", err) } if response.Error != nil { - return nil, errors.Errorf("Response error: %v", response.Error) + return nil, errors.Errorf("response error: %v", response.Error) + } + // From the JSON-RPC 2.0 spec: + // id: It MUST be the same as the value of the id member in the Request Object. + if err := validateResponseID(response, expectedID); err != nil { + return nil, err } // Unmarshal the RawMessage into the result. err = cdc.UnmarshalJSON(response.Result, result) if err != nil { - return nil, errors.Errorf("Error unmarshalling rpc response result: %v", err) + return nil, errors.Errorf("error unmarshalling rpc response result: %v", err) } return result, nil } +func unmarshalResponseBytesArray(cdc *amino.Codec, responseBytes []byte, expectedID types.JSONRPCStringID, results []interface{}) ([]interface{}, error) { + var ( + err error + responses []types.RPCResponse + ) + err = json.Unmarshal(responseBytes, &responses) + if err != nil { + return nil, errors.Errorf("error unmarshalling rpc response: %v", err) + } + // No response error checking here as there may be a mixture of successful + // and unsuccessful responses. + + if len(results) != len(responses) { + return nil, errors.Errorf("expected %d result objects into which to inject responses, but got %d", len(responses), len(results)) + } + + for i, response := range responses { + // From the JSON-RPC 2.0 spec: + // id: It MUST be the same as the value of the id member in the Request Object. + if err := validateResponseID(&response, expectedID); err != nil { + return nil, errors.Errorf("failed to validate response ID in response %d: %v", i, err) + } + if err := cdc.UnmarshalJSON(responses[i].Result, results[i]); err != nil { + return nil, errors.Errorf("error unmarshalling rpc response result: %v", err) + } + } + return results, nil +} + +func validateResponseID(res *types.RPCResponse, expectedID types.JSONRPCStringID) error { + // we only validate a response ID if the expected ID is non-empty + if len(expectedID) == 0 { + return nil + } + if res.ID == nil { + return errors.Errorf("missing ID in response") + } + id, ok := res.ID.(types.JSONRPCStringID) + if !ok { + return errors.Errorf("expected ID string in response but got: %v", id) + } + if expectedID != id { + return errors.Errorf("response ID (%s) does not match request ID (%s)", id, expectedID) + } + return nil +} + func argsToURLValues(cdc *amino.Codec, args map[string]interface{}) (url.Values, error) { values := make(url.Values) if len(args) == 0 { diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index 6391b0090..c1c1ebf1a 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -103,7 +103,7 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, cdc *amino.Codec, logger lo return func(w http.ResponseWriter, r *http.Request) { b, err := ioutil.ReadAll(r.Body) if err != nil { - WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(types.JSONRPCStringID(""), errors.Wrap(err, "Error reading request body"))) + WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(types.JSONRPCStringID(""), errors.Wrap(err, "error reading request body"))) return } // if its an empty request (like from a browser), @@ -113,49 +113,59 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, cdc *amino.Codec, logger lo return } - var request types.RPCRequest - err = json.Unmarshal(b, &request) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "Error unmarshalling request"))) - return - } - // A Notification is a Request object without an "id" member. - // The Server MUST NOT reply to a Notification, including those that are within a batch request. - if request.ID == types.JSONRPCStringID("") { - logger.Debug("HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") - return - } - if len(r.URL.Path) > 1 { - WriteRPCResponseHTTP(w, types.RPCInvalidRequestError(request.ID, errors.Errorf("Path %s is invalid", r.URL.Path))) - return - } - - rpcFunc := funcMap[request.Method] - if rpcFunc == nil || rpcFunc.ws { - WriteRPCResponseHTTP(w, types.RPCMethodNotFoundError(request.ID)) - return + // first try to unmarshal the incoming request as an array of RPC requests + var ( + requests []types.RPCRequest + responses []types.RPCResponse + ) + if err := json.Unmarshal(b, &requests); err != nil { + // next, try to unmarshal as a single request + var request types.RPCRequest + if err := json.Unmarshal(b, &request); err != nil { + WriteRPCResponseHTTP(w, types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))) + return + } + requests = []types.RPCRequest{request} } - ctx := &types.Context{JSONReq: &request, HTTPReq: r} - args := []reflect.Value{reflect.ValueOf(ctx)} - if len(request.Params) > 0 { - fnArgs, err := jsonParamsToArgs(rpcFunc, cdc, request.Params) + for _, request := range requests { + // A Notification is a Request object without an "id" member. + // The Server MUST NOT reply to a Notification, including those that are within a batch request. + if request.ID == types.JSONRPCStringID("") { + logger.Debug("HTTPJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") + continue + } + if len(r.URL.Path) > 1 { + responses = append(responses, types.RPCInvalidRequestError(request.ID, errors.Errorf("path %s is invalid", r.URL.Path))) + continue + } + rpcFunc, ok := funcMap[request.Method] + if !ok || rpcFunc.ws { + responses = append(responses, types.RPCMethodNotFoundError(request.ID)) + continue + } + ctx := &types.Context{JSONReq: &request, HTTPReq: r} + args := []reflect.Value{reflect.ValueOf(ctx)} + if len(request.Params) > 0 { + fnArgs, err := jsonParamsToArgs(rpcFunc, cdc, request.Params) + if err != nil { + responses = append(responses, types.RPCInvalidParamsError(request.ID, errors.Wrap(err, "error converting json params to arguments"))) + continue + } + args = append(args, fnArgs...) + } + returns := rpcFunc.f.Call(args) + logger.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) + result, err := unreflectResult(returns) if err != nil { - WriteRPCResponseHTTP(w, types.RPCInvalidParamsError(request.ID, errors.Wrap(err, "Error converting json params to arguments"))) - return + responses = append(responses, types.RPCInternalError(request.ID, err)) + continue } - args = append(args, fnArgs...) + responses = append(responses, types.NewRPCSuccessResponse(cdc, request.ID, result)) } - - returns := rpcFunc.f.Call(args) - - logger.Info("HTTPJSONRPC", "method", request.Method, "args", args, "returns", returns) - result, err := unreflectResult(returns) - if err != nil { - WriteRPCResponseHTTP(w, types.RPCInternalError(request.ID, err)) - return + if len(responses) > 0 { + WriteRPCResponseArrayHTTP(w, responses) } - WriteRPCResponseHTTP(w, types.NewRPCSuccessResponse(cdc, request.ID, result)) } } @@ -194,7 +204,7 @@ func mapParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, params map[string]json. func arrayParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, params []json.RawMessage, argsOffset int) ([]reflect.Value, error) { if len(rpcFunc.argNames) != len(params) { - return nil, errors.Errorf("Expected %v parameters (%v), got %v (%v)", + return nil, errors.Errorf("expected %v parameters (%v), got %v (%v)", len(rpcFunc.argNames), rpcFunc.argNames, len(params), params) } @@ -236,7 +246,7 @@ func jsonParamsToArgs(rpcFunc *RPCFunc, cdc *amino.Codec, raw []byte) ([]reflect } // Otherwise, bad format, we cannot parse - return nil, errors.Errorf("Unknown type for JSON params: %v. Expected map or array", err) + return nil, errors.Errorf("unknown type for JSON params: %v. Expected map or array", err) } // rpc.json @@ -261,7 +271,7 @@ func makeHTTPHandler(rpcFunc *RPCFunc, cdc *amino.Codec, logger log.Logger) func fnArgs, err := httpParamsToArgs(rpcFunc, cdc, r) if err != nil { - WriteRPCResponseHTTP(w, types.RPCInvalidParamsError(types.JSONRPCStringID(""), errors.Wrap(err, "Error converting http params to arguments"))) + WriteRPCResponseHTTP(w, types.RPCInvalidParamsError(types.JSONRPCStringID(""), errors.Wrap(err, "error converting http params to arguments"))) return } args = append(args, fnArgs...) @@ -372,7 +382,7 @@ func _nonJSONStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect if isHexString { if !expectingString && !expectingByteSlice { - err := errors.Errorf("Got a hex string arg, but expected '%s'", + err := errors.Errorf("got a hex string arg, but expected '%s'", rt.Kind().String()) return reflect.ValueOf(nil), err, false } @@ -631,7 +641,7 @@ func (wsc *wsConnection) readRoutine() { var request types.RPCRequest err = json.Unmarshal(in, &request) if err != nil { - wsc.WriteRPCResponse(types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "Error unmarshaling request"))) + wsc.WriteRPCResponse(types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshaling request"))) continue } @@ -654,7 +664,7 @@ func (wsc *wsConnection) readRoutine() { if len(request.Params) > 0 { fnArgs, err := jsonParamsToArgs(rpcFunc, wsc.cdc, request.Params) if err != nil { - wsc.WriteRPCResponse(types.RPCInternalError(request.ID, errors.Wrap(err, "Error converting json params to arguments"))) + wsc.WriteRPCResponse(types.RPCInternalError(request.ID, errors.Wrap(err, "error converting json params to arguments"))) continue } args = append(args, fnArgs...) diff --git a/rpc/lib/server/handlers_test.go b/rpc/lib/server/handlers_test.go index f8ad06107..9cded2953 100644 --- a/rpc/lib/server/handlers_test.go +++ b/rpc/lib/server/handlers_test.go @@ -154,6 +154,72 @@ func TestRPCNotification(t *testing.T) { require.Equal(t, len(blob), 0, "a notification SHOULD NOT be responded to by the server") } +func TestRPCNotificationInBatch(t *testing.T) { + mux := testMux() + tests := []struct { + payload string + expectCount int + }{ + { + `[ + {"jsonrpc": "2.0","id": ""}, + {"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]} + ]`, + 1, + }, + { + `[ + {"jsonrpc": "2.0","id": ""}, + {"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]}, + {"jsonrpc": "2.0","id": ""}, + {"jsonrpc": "2.0","method":"c","id":"abc","params":["a","10"]} + ]`, + 2, + }, + } + for i, tt := range tests { + req, _ := http.NewRequest("POST", "http://localhost/", strings.NewReader(tt.payload)) + rec := httptest.NewRecorder() + mux.ServeHTTP(rec, req) + res := rec.Result() + // Always expecting back a JSONRPCResponse + assert.True(t, statusOK(res.StatusCode), "#%d: should always return 2XX", i) + blob, err := ioutil.ReadAll(res.Body) + if err != nil { + t.Errorf("#%d: err reading body: %v", i, err) + continue + } + + var responses []types.RPCResponse + // try to unmarshal an array first + err = json.Unmarshal(blob, &responses) + if err != nil { + // if we were actually expecting an array, but got an error + if tt.expectCount > 1 { + t.Errorf("#%d: expected an array, couldn't unmarshal it\nblob: %s", i, blob) + continue + } else { + // we were expecting an error here, so let's unmarshal a single response + var response types.RPCResponse + err = json.Unmarshal(blob, &response) + if err != nil { + t.Errorf("#%d: expected successful parsing of an RPCResponse\nblob: %s", i, blob) + continue + } + // have a single-element result + responses = []types.RPCResponse{response} + } + } + if tt.expectCount != len(responses) { + t.Errorf("#%d: expected %d response(s), but got %d\nblob: %s", i, tt.expectCount, len(responses), blob) + continue + } + for _, response := range responses { + assert.NotEqual(t, response, new(types.RPCResponse), "#%d: not expecting a blank RPCResponse", i) + } + } +} + func TestUnknownRPCPath(t *testing.T) { mux := testMux() req, _ := http.NewRequest("GET", "http://localhost/unknownrpcpath", nil) diff --git a/rpc/lib/server/http_params.go b/rpc/lib/server/http_params.go index 3c948c0ba..8ade41c79 100644 --- a/rpc/lib/server/http_params.go +++ b/rpc/lib/server/http_params.go @@ -76,7 +76,7 @@ func GetParamUint(r *http.Request, param string) (uint, error) { func GetParamRegexp(r *http.Request, param string, re *regexp.Regexp) (string, error) { s := GetParam(r, param) if !re.MatchString(s) { - return "", errors.Errorf(param, "Did not match regular expression %v", re.String()) + return "", errors.Errorf(param, "did not match regular expression %v", re.String()) } return s, nil } diff --git a/rpc/lib/server/http_server.go b/rpc/lib/server/http_server.go index c4bb6fa17..7825605eb 100644 --- a/rpc/lib/server/http_server.go +++ b/rpc/lib/server/http_server.go @@ -98,7 +98,9 @@ func WriteRPCResponseHTTPError( w.Header().Set("Content-Type", "application/json") w.WriteHeader(httpCode) - w.Write(jsonBytes) // nolint: errcheck, gas + if _, err := w.Write(jsonBytes); err != nil { + panic(err) + } } func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { @@ -108,12 +110,33 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { } w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) - w.Write(jsonBytes) // nolint: errcheck, gas + if _, err := w.Write(jsonBytes); err != nil { + panic(err) + } +} + +// WriteRPCResponseArrayHTTP will do the same as WriteRPCResponseHTTP, except it +// can write arrays of responses for batched request/response interactions via +// the JSON RPC. +func WriteRPCResponseArrayHTTP(w http.ResponseWriter, res []types.RPCResponse) { + if len(res) == 1 { + WriteRPCResponseHTTP(w, res[0]) + } else { + jsonBytes, err := json.MarshalIndent(res, "", " ") + if err != nil { + panic(err) + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + if _, err := w.Write(jsonBytes); err != nil { + panic(err) + } + } } //----------------------------------------------------------------------------- -// Wraps an HTTP handler, adding error logging. +// RecoverAndLogHandler wraps an HTTP handler, adding error logging. // If the inner function panics, the outer function recovers, logs, sends an // HTTP 500 error response. func RecoverAndLogHandler(handler http.Handler, logger log.Logger) http.Handler { @@ -191,14 +214,14 @@ func Listen(addr string, config *Config) (listener net.Listener, err error) { parts := strings.SplitN(addr, "://", 2) if len(parts) != 2 { return nil, errors.Errorf( - "Invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)", + "invalid listening address %s (use fully formed addresses, including the tcp:// or unix:// prefix)", addr, ) } proto, addr := parts[0], parts[1] listener, err = net.Listen(proto, addr) if err != nil { - return nil, errors.Errorf("Failed to listen on %v: %v", addr, err) + return nil, errors.Errorf("failed to listen on %v: %v", addr, err) } if config.MaxOpenConnections > 0 { listener = netutil.LimitListener(listener, config.MaxOpenConnections) diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index 10d165625..cddc80b8e 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -23,7 +23,18 @@ import ( rpcclient "github.com/tendermint/tendermint/rpc/lib/client" ) +// Options helps with specifying some parameters for our RPC testing for greater +// control. +type Options struct { + suppressStdout bool + recreateConfig bool +} + var globalConfig *cfg.Config +var defaultOptions = Options{ + suppressStdout: false, + recreateConfig: false, +} func waitForRPC() { laddr := GetConfig().RPC.ListenAddress @@ -77,19 +88,24 @@ func makeAddrs() (string, string, string) { fmt.Sprintf("tcp://0.0.0.0:%d", randPort()) } +func createConfig() *cfg.Config { + pathname := makePathname() + c := cfg.ResetTestRoot(pathname) + + // and we use random ports to run in parallel + tm, rpc, grpc := makeAddrs() + c.P2P.ListenAddress = tm + c.RPC.ListenAddress = rpc + c.RPC.CORSAllowedOrigins = []string{"https://tendermint.com/"} + c.RPC.GRPCListenAddress = grpc + c.TxIndex.IndexTags = "app.creator,tx.height" // see kvstore application + return c +} + // GetConfig returns a config for the test cases as a singleton -func GetConfig() *cfg.Config { - if globalConfig == nil { - pathname := makePathname() - globalConfig = cfg.ResetTestRoot(pathname) - - // and we use random ports to run in parallel - tm, rpc, grpc := makeAddrs() - globalConfig.P2P.ListenAddress = tm - globalConfig.RPC.ListenAddress = rpc - globalConfig.RPC.CORSAllowedOrigins = []string{"https://tendermint.com/"} - globalConfig.RPC.GRPCListenAddress = grpc - globalConfig.TxIndex.IndexTags = "app.creator,tx.height" // see kvstore application +func GetConfig(forceCreate ...bool) *cfg.Config { + if globalConfig == nil || (len(forceCreate) > 0 && forceCreate[0]) { + globalConfig = createConfig() } return globalConfig } @@ -100,8 +116,12 @@ func GetGRPCClient() core_grpc.BroadcastAPIClient { } // StartTendermint starts a test tendermint server in a go routine and returns when it is initialized -func StartTendermint(app abci.Application) *nm.Node { - node := NewTendermint(app) +func StartTendermint(app abci.Application, opts ...func(*Options)) *nm.Node { + nodeOpts := defaultOptions + for _, opt := range opts { + opt(&nodeOpts) + } + node := NewTendermint(app, &nodeOpts) err := node.Start() if err != nil { panic(err) @@ -111,7 +131,9 @@ func StartTendermint(app abci.Application) *nm.Node { waitForRPC() waitForGRPC() - fmt.Println("Tendermint running!") + if !nodeOpts.suppressStdout { + fmt.Println("Tendermint running!") + } return node } @@ -125,11 +147,16 @@ func StopTendermint(node *nm.Node) { } // NewTendermint creates a new tendermint server and sleeps forever -func NewTendermint(app abci.Application) *nm.Node { +func NewTendermint(app abci.Application, opts *Options) *nm.Node { // Create & start node - config := GetConfig() - logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - logger = log.NewFilter(logger, log.AllowError()) + config := GetConfig(opts.recreateConfig) + var logger log.Logger + if opts.suppressStdout { + logger = log.NewNopLogger() + } else { + logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + logger = log.NewFilter(logger, log.AllowError()) + } pvKeyFile := config.PrivValidatorKeyFile() pvKeyStateFile := config.PrivValidatorStateFile() pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) @@ -148,3 +175,15 @@ func NewTendermint(app abci.Application) *nm.Node { } return node } + +// SuppressStdout is an option that tries to make sure the RPC test Tendermint +// node doesn't log anything to stdout. +func SuppressStdout(o *Options) { + o.suppressStdout = true +} + +// RecreateConfig instructs the RPC test to recreate the configuration each +// time, instead of treating it as a global singleton. +func RecreateConfig(o *Options) { + o.recreateConfig = true +} From f2aa1bf50e3eb61f9cf27ee4e867fbd1daf9069f Mon Sep 17 00:00:00 2001 From: kevlubkcm <36485490+kevlubkcm@users.noreply.github.com> Date: Wed, 17 Apr 2019 12:14:01 -0400 Subject: [PATCH 004/211] bandaid for non-deterministic clist test (#3575) * add a deterministic timeout Co-Authored-By: kevlubkcm <36485490+kevlubkcm@users.noreply.github.com> --- libs/clist/clist_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/clist/clist_test.go b/libs/clist/clist_test.go index 13aca3577..1784f8218 100644 --- a/libs/clist/clist_test.go +++ b/libs/clist/clist_test.go @@ -261,6 +261,8 @@ func TestWaitChan(t *testing.T) { pushed++ time.Sleep(time.Duration(cmn.RandIntn(25)) * time.Millisecond) } + // apply a deterministic pause so the counter has time to catch up + time.Sleep(25 * time.Millisecond) close(done) }() @@ -273,7 +275,7 @@ FOR_LOOP: next = next.Next() seen++ if next == nil { - continue + t.Fatal("Next should not be nil when waiting on NextWaitChan") } case <-done: break FOR_LOOP From 671c5c9b84644b4b83b00fbf23f8d3dd7ff5c199 Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Thu, 18 Apr 2019 17:31:36 +0200 Subject: [PATCH 005/211] crypto: Proof of Concept for iterative version of SimpleHashFromByteSlices (#2611) (#3530) (#2611) had suggested that an iterative version of SimpleHashFromByteSlice would be faster, presumably because we can envision some overhead accumulating from stack frames and function calls. Additionally, a recursive algorithm risks hitting the stack limit and causing a stack overflow should the tree be too large. Provided here is an iterative alternative, a simple test to assert correctness and a benchmark. On the performance side, there appears to be no overall difference: ``` BenchmarkSimpleHashAlternatives/recursive-4 20000 77677 ns/op BenchmarkSimpleHashAlternatives/iterative-4 20000 76802 ns/op ``` On the surface it might seem that the additional overhead is due to the different allocation patterns of the implementations. The recursive version uses a single `[][]byte` slices which it then re-slices at each level of the tree. The iterative version reproduces `[][]byte` once within the function and then rewrites sub-slices of that array at each level of the tree. Eexperimenting by modifying the code to simply calculate the hash and not store the result show little to no difference in performance. These preliminary results suggest: 1. The performance of the current implementation is pretty good 2. Go has low overhead for recursive functions 3. The performance of the SimpleHashFromByteSlice routine is dominated by the actual hashing of data Although this work is in no way exhaustive, point #3 suggests that optimizations of this routine would need to take an alternative approach to make significant improvements on the current performance. Finally, considering that the recursive implementation is easier to read, it might not be worthwhile to switch to a less intuitive implementation for so little benefit. * re-add slice re-writing * [crypto] Document SimpleHashFromByteSlicesIterative --- crypto/merkle/simple_tree.go | 71 +++++++++++++++++++++++++++++++ crypto/merkle/simple_tree_test.go | 36 ++++++++++++++++ 2 files changed, 107 insertions(+) diff --git a/crypto/merkle/simple_tree.go b/crypto/merkle/simple_tree.go index 5de514b51..03dc9d9d1 100644 --- a/crypto/merkle/simple_tree.go +++ b/crypto/merkle/simple_tree.go @@ -20,6 +20,77 @@ func SimpleHashFromByteSlices(items [][]byte) []byte { } } +// SimpleHashFromByteSliceIterative is an iterative alternative to +// SimpleHashFromByteSlice motivated by potential performance improvements. +// (#2611) had suggested that an iterative version of +// SimpleHashFromByteSlice would be faster, presumably because +// we can envision some overhead accumulating from stack +// frames and function calls. Additionally, a recursive algorithm risks +// hitting the stack limit and causing a stack overflow should the tree +// be too large. +// +// Provided here is an iterative alternative, a simple test to assert +// correctness and a benchmark. On the performance side, there appears to +// be no overall difference: +// +// BenchmarkSimpleHashAlternatives/recursive-4 20000 77677 ns/op +// BenchmarkSimpleHashAlternatives/iterative-4 20000 76802 ns/op +// +// On the surface it might seem that the additional overhead is due to +// the different allocation patterns of the implementations. The recursive +// version uses a single [][]byte slices which it then re-slices at each level of the tree. +// The iterative version reproduces [][]byte once within the function and +// then rewrites sub-slices of that array at each level of the tree. +// +// Experimenting by modifying the code to simply calculate the +// hash and not store the result show little to no difference in performance. +// +// These preliminary results suggest: +// +// 1. The performance of the SimpleHashFromByteSlice is pretty good +// 2. Go has low overhead for recursive functions +// 3. The performance of the SimpleHashFromByteSlice routine is dominated +// by the actual hashing of data +// +// Although this work is in no way exhaustive, point #3 suggests that +// optimization of this routine would need to take an alternative +// approach to make significant improvements on the current performance. +// +// Finally, considering that the recursive implementation is easier to +// read, it might not be worthwhile to switch to a less intuitive +// implementation for so little benefit. +func SimpleHashFromByteSlicesIterative(input [][]byte) []byte { + items := make([][]byte, len(input)) + + for i, leaf := range input { + items[i] = leafHash(leaf) + } + + size := len(items) + for { + switch size { + case 0: + return nil + case 1: + return items[0] + default: + rp := 0 // read position + wp := 0 // write position + for rp < size { + if rp+1 < size { + items[wp] = innerHash(items[rp], items[rp+1]) + rp += 2 + } else { + items[wp] = items[rp] + rp += 1 + } + wp += 1 + } + size = wp + } + } +} + // SimpleHashFromMap computes a Merkle tree from sorted map. // Like calling SimpleHashFromHashers with // `item = []byte(Hash(key) | Hash(value))`, diff --git a/crypto/merkle/simple_tree_test.go b/crypto/merkle/simple_tree_test.go index 9abe321c3..5bbe294af 100644 --- a/crypto/merkle/simple_tree_test.go +++ b/crypto/merkle/simple_tree_test.go @@ -70,6 +70,42 @@ func TestSimpleProof(t *testing.T) { } } +func TestSimpleHashAlternatives(t *testing.T) { + + total := 100 + + items := make([][]byte, total) + for i := 0; i < total; i++ { + items[i] = testItem(cmn.RandBytes(tmhash.Size)) + } + + rootHash1 := SimpleHashFromByteSlicesIterative(items) + rootHash2 := SimpleHashFromByteSlices(items) + require.Equal(t, rootHash1, rootHash2, "Unmatched root hashes: %X vs %X", rootHash1, rootHash2) +} + +func BenchmarkSimpleHashAlternatives(b *testing.B) { + total := 100 + + items := make([][]byte, total) + for i := 0; i < total; i++ { + items[i] = testItem(cmn.RandBytes(tmhash.Size)) + } + + b.ResetTimer() + b.Run("recursive", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = SimpleHashFromByteSlices(items) + } + }) + + b.Run("iterative", func(b *testing.B) { + for i := 0; i < b.N; i++ { + _ = SimpleHashFromByteSlicesIterative(items) + } + }) +} + func Test_getSplitPoint(t *testing.T) { tests := []struct { length int From 8db7e74b873712b4bb3c72616a798d3fb82094e9 Mon Sep 17 00:00:00 2001 From: Ismail Khoffi Date: Mon, 22 Apr 2019 10:04:04 +0200 Subject: [PATCH 006/211] privval: increase timeout to mitigate non-deterministic test failure (#3580) This should fix #3576 (ran it many times locally but only time will tell). The test actually only checked for the opcode of the error. From the name of the test we actually want to test if we see a timeout after a pre-defined time. ## Commits: * increase readWrite timeout as it is also used in the `Accept` of the tcp listener: - before this caused the readWriteTimeout to kick in (rarely) while Accept - as a side-effect: remove obsolete time.Sleep: in both listener cases the Accept will only return after successfully accepting and the timeout that is supposed to be tested here will be triggered because there is a read without a write - see if we actually run into a timeout error (the whole purpose of this test AFAIU) Signed-off-by: Ismail Khoffi * makee local test-vars `const` Signed-off-by: Ismail Khoffi ## Additional comments: @xla: Confusing how an accept could take longer than that, but assuming a noisy environment full of little docker whales will be slower than what 50 years of silicon are capable of. The only thing I'd be vary of is that we mask structural issues with the code by just bumping the timeout, if we are sensitive towards that it warrants invesigation, but again this might only be true in the environment our CI runs in. --- privval/socket_listeners_test.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/privval/socket_listeners_test.go b/privval/socket_listeners_test.go index 498ef79c0..1cda2b6ec 100644 --- a/privval/socket_listeners_test.go +++ b/privval/socket_listeners_test.go @@ -97,7 +97,15 @@ func TestListenerTimeoutAccept(t *testing.T) { } func TestListenerTimeoutReadWrite(t *testing.T) { - for _, tc := range listenerTestCases(t, time.Second, time.Millisecond) { + const ( + // This needs to be long enough s.t. the Accept will definitely succeed: + timeoutAccept = time.Second + // This can be really short but in the TCP case, the accept can + // also trigger a timeoutReadWrite. Hence, we need to give it some time. + // Note: this controls how long this test actually runs. + timeoutReadWrite = 10 * time.Millisecond + ) + for _, tc := range listenerTestCases(t, timeoutAccept, timeoutReadWrite) { go func(dialer SocketDialer) { _, err := dialer() if err != nil { @@ -110,8 +118,7 @@ func TestListenerTimeoutReadWrite(t *testing.T) { t.Fatal(err) } - time.Sleep(2 * time.Millisecond) - + // this will timeout because we don't write anything: msg := make([]byte, 200) _, err = c.Read(msg) opErr, ok := err.(*net.OpError) @@ -122,5 +129,9 @@ func TestListenerTimeoutReadWrite(t *testing.T) { if have, want := opErr.Op, "read"; have != want { t.Errorf("for %s listener, have %v, want %v", tc.description, have, want) } + + if have, want := opErr.Timeout(), true; have != want { + t.Errorf("for %s listener, got unexpected error: have %v, want %v", tc.description, have, want) + } } } From ebf815ee572ab47ee0945a5f5f8276c9f242b2fa Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 23 Apr 2019 12:22:40 +0400 Subject: [PATCH 007/211] cs/replay: check appHash for each block (#3579) * check every block appHash Fixes #3460 Not really needed, but it would detect if the app changed in a way it shouldn't have. * add a changelog entry * no need to return an error if we panic * rename methods * rename methods once again * add a test * correct error msg plus fix a few go-lint warnings * better panic msg --- CHANGELOG_PENDING.md | 1 + consensus/replay.go | 58 ++++++++--- consensus/replay_test.go | 219 ++++++++++++++++++++++++++++++++------- 3 files changed, 223 insertions(+), 55 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 3f151f71a..e05430218 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -18,6 +18,7 @@ ### IMPROVEMENTS: - [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC +- [cs/replay] \#3460 check appHash for each block ### BUG FIXES: - [state] [\#3537](https://github.com/tendermint/tendermint/pull/3537#issuecomment-482711833) LoadValidators: do not return an empty validator set diff --git a/consensus/replay.go b/consensus/replay.go index e47d4892a..38ed79fcf 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -13,7 +13,7 @@ import ( abci "github.com/tendermint/tendermint/abci/types" //auto "github.com/tendermint/tendermint/libs/autofile" - cmn "github.com/tendermint/tendermint/libs/common" + dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" @@ -230,6 +230,7 @@ func (h *Handshaker) SetEventBus(eventBus types.BlockEventPublisher) { h.eventBus = eventBus } +// NBlocks returns the number of blocks applied to the state. func (h *Handshaker) NBlocks() int { return h.nBlocks } @@ -263,7 +264,7 @@ func (h *Handshaker) Handshake(proxyApp proxy.AppConns) error { // Replay blocks up to the latest in the blockstore. _, err = h.ReplayBlocks(h.initialState, appHash, blockHeight, proxyApp) if err != nil { - return fmt.Errorf("Error on replay: %v", err) + return fmt.Errorf("error on replay: %v", err) } h.logger.Info("Completed ABCI Handshake - Tendermint and App are synced", @@ -274,7 +275,8 @@ func (h *Handshaker) Handshake(proxyApp proxy.AppConns) error { return nil } -// Replay all blocks since appBlockHeight and ensure the result matches the current state. +// ReplayBlocks replays all blocks since appBlockHeight and ensures the result +// matches the current state. // Returns the final AppHash or an error. func (h *Handshaker) ReplayBlocks( state sm.State, @@ -319,7 +321,7 @@ func (h *Handshaker) ReplayBlocks( } else { // If validator set is not set in genesis and still empty after InitChain, exit. if len(h.genDoc.Validators) == 0 { - return nil, fmt.Errorf("Validator set is nil in genesis and still empty after InitChain") + return nil, fmt.Errorf("validator set is nil in genesis and still empty after InitChain") } } @@ -332,7 +334,8 @@ func (h *Handshaker) ReplayBlocks( // First handle edge cases and constraints on the storeBlockHeight. if storeBlockHeight == 0 { - return appHash, checkAppHash(state, appHash) + assertAppHashEqualsOneFromState(appHash, state) + return appHash, nil } else if storeBlockHeight < appBlockHeight { // the app should never be ahead of the store (but this is under app's control) @@ -340,11 +343,11 @@ func (h *Handshaker) ReplayBlocks( } else if storeBlockHeight < stateBlockHeight { // the state should never be ahead of the store (this is under tendermint's control) - cmn.PanicSanity(fmt.Sprintf("StateBlockHeight (%d) > StoreBlockHeight (%d)", stateBlockHeight, storeBlockHeight)) + panic(fmt.Sprintf("StateBlockHeight (%d) > StoreBlockHeight (%d)", stateBlockHeight, storeBlockHeight)) } else if storeBlockHeight > stateBlockHeight+1 { // store should be at most one ahead of the state (this is under tendermint's control) - cmn.PanicSanity(fmt.Sprintf("StoreBlockHeight (%d) > StateBlockHeight + 1 (%d)", storeBlockHeight, stateBlockHeight+1)) + panic(fmt.Sprintf("StoreBlockHeight (%d) > StateBlockHeight + 1 (%d)", storeBlockHeight, stateBlockHeight+1)) } var err error @@ -359,7 +362,8 @@ func (h *Handshaker) ReplayBlocks( } else if appBlockHeight == storeBlockHeight { // We're good! - return appHash, checkAppHash(state, appHash) + assertAppHashEqualsOneFromState(appHash, state) + return appHash, nil } } else if storeBlockHeight == stateBlockHeight+1 { @@ -380,7 +384,7 @@ func (h *Handshaker) ReplayBlocks( return state.AppHash, err } else if appBlockHeight == storeBlockHeight { - // We ran Commit, but didn't save the state, so replayBlock with mock app + // We ran Commit, but didn't save the state, so replayBlock with mock app. abciResponses, err := sm.LoadABCIResponses(h.stateDB, storeBlockHeight) if err != nil { return nil, err @@ -393,8 +397,8 @@ func (h *Handshaker) ReplayBlocks( } - cmn.PanicSanity("Should never happen") - return nil, nil + panic(fmt.Sprintf("uncovered case! appHeight: %d, storeHeight: %d, stateHeight: %d", + appBlockHeight, storeBlockHeight, stateBlockHeight)) } func (h *Handshaker) replayBlocks(state sm.State, proxyApp proxy.AppConns, appBlockHeight, storeBlockHeight int64, mutateState bool) ([]byte, error) { @@ -417,6 +421,12 @@ func (h *Handshaker) replayBlocks(state sm.State, proxyApp proxy.AppConns, appBl for i := appBlockHeight + 1; i <= finalBlock; i++ { h.logger.Info("Applying block", "height", i) block := h.store.LoadBlock(i) + + // Extra check to ensure the app was not changed in a way it shouldn't have. + if len(appHash) > 0 { + assertAppHashEqualsOneFromBlock(appHash, block) + } + appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, state.LastValidators, h.stateDB) if err != nil { return nil, err @@ -434,7 +444,8 @@ func (h *Handshaker) replayBlocks(state sm.State, proxyApp proxy.AppConns, appBl appHash = state.AppHash } - return appHash, checkAppHash(state, appHash) + assertAppHashEqualsOneFromState(appHash, state) + return appHash, nil } // ApplyBlock on the proxyApp with the last block. @@ -456,11 +467,26 @@ func (h *Handshaker) replayBlock(state sm.State, height int64, proxyApp proxy.Ap return state, nil } -func checkAppHash(state sm.State, appHash []byte) error { - if !bytes.Equal(state.AppHash, appHash) { - panic(fmt.Errorf("Tendermint state.AppHash does not match AppHash after replay. Got %X, expected %X", appHash, state.AppHash).Error()) +func assertAppHashEqualsOneFromBlock(appHash []byte, block *types.Block) { + if !bytes.Equal(appHash, block.AppHash) { + panic(fmt.Sprintf(`block.AppHash does not match AppHash after replay. Got %X, expected %X. + +Block: %v +`, + appHash, block.AppHash, block)) + } +} + +func assertAppHashEqualsOneFromState(appHash []byte, state sm.State) { + if !bytes.Equal(appHash, state.AppHash) { + panic(fmt.Sprintf(`state.AppHash does not match AppHash after replay. Got +%X, expected %X. + +State: %v + +Did you reset Tendermint without resetting your application's data?`, + appHash, state.AppHash, state)) } - return nil } //-------------------------------------------------------------------------------- diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 86dca7657..b084cef4e 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -7,7 +7,7 @@ import ( "io" "io/ioutil" "os" - "path" + "path/filepath" "runtime" "testing" "time" @@ -19,12 +19,14 @@ import ( abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/crypto" + cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" ) @@ -88,7 +90,7 @@ func startNewConsensusStateAndWaitForBlock(t *testing.T, consensusReplayConfig * } } -func sendTxs(cs *ConsensusState, ctx context.Context) { +func sendTxs(ctx context.Context, cs *ConsensusState) { for i := 0; i < 256; i++ { select { case <-ctx.Done(): @@ -113,7 +115,7 @@ func TestWALCrash(t *testing.T) { 1}, {"many non-empty blocks", func(stateDB dbm.DB, cs *ConsensusState, ctx context.Context) { - go sendTxs(cs, ctx) + go sendTxs(ctx, cs) }, 3}, } @@ -263,7 +265,7 @@ func (w *crashingWAL) Wait() { w.next.Wait() } // Handshake Tests const ( - NUM_BLOCKS = 6 + numBlocks = 6 ) var ( @@ -284,6 +286,7 @@ func TestHandshakeReplayAll(t *testing.T) { for i, m := range modes { config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) defer os.RemoveAll(config.RootDir) + testHandshakeReplay(t, config, 0, m) } } @@ -293,6 +296,7 @@ func TestHandshakeReplaySome(t *testing.T) { for i, m := range modes { config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) defer os.RemoveAll(config.RootDir) + testHandshakeReplay(t, config, 1, m) } } @@ -302,7 +306,8 @@ func TestHandshakeReplayOne(t *testing.T) { for i, m := range modes { config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) defer os.RemoveAll(config.RootDir) - testHandshakeReplay(t, config, NUM_BLOCKS-1, m) + + testHandshakeReplay(t, config, numBlocks-1, m) } } @@ -311,28 +316,30 @@ func TestHandshakeReplayNone(t *testing.T) { for i, m := range modes { config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) defer os.RemoveAll(config.RootDir) - testHandshakeReplay(t, config, NUM_BLOCKS, m) + + testHandshakeReplay(t, config, numBlocks, m) } } func tempWALWithData(data []byte) string { walFile, err := ioutil.TempFile("", "wal") if err != nil { - panic(fmt.Errorf("failed to create temp WAL file: %v", err)) + panic(fmt.Sprintf("failed to create temp WAL file: %v", err)) } _, err = walFile.Write(data) if err != nil { - panic(fmt.Errorf("failed to write to temp WAL file: %v", err)) + panic(fmt.Sprintf("failed to write to temp WAL file: %v", err)) } if err := walFile.Close(); err != nil { - panic(fmt.Errorf("failed to close temp WAL file: %v", err)) + panic(fmt.Sprintf("failed to close temp WAL file: %v", err)) } return walFile.Name() } -// Make some blocks. Start a fresh app and apply nBlocks blocks. Then restart the app and sync it up with the remaining blocks +// Make some blocks. Start a fresh app and apply nBlocks blocks. Then restart +// the app and sync it up with the remaining blocks. func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uint) { - walBody, err := WALWithNBlocks(t, NUM_BLOCKS) + walBody, err := WALWithNBlocks(t, numBlocks) require.NoError(t, err) walFile := tempWALWithData(walBody) config.Consensus.SetWalFile(walFile) @@ -354,11 +361,16 @@ func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uin store.commits = commits // run the chain through state.ApplyBlock to build up the tendermint state - state = buildTMStateFromChain(config, stateDB, state, chain, mode) + clientCreator := proxy.NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), "1"))) + proxyApp := proxy.NewAppConns(clientCreator) + err = proxyApp.Start() + require.NoError(t, err) + state = buildTMStateFromChain(config, stateDB, state, chain, proxyApp, mode) + proxyApp.Stop() latestAppHash := state.AppHash // make a new client creator - kvstoreApp := kvstore.NewPersistentKVStoreApplication(path.Join(config.DBDir(), "2")) + kvstoreApp := kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), "2")) clientCreator2 := proxy.NewLocalClientCreator(kvstoreApp) if nBlocks > 0 { // run nBlocks against a new client to build up the app state. @@ -371,7 +383,7 @@ func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uin // now start the app using the handshake - it should sync genDoc, _ := sm.MakeGenesisDocFromFile(config.GenesisFile()) handshaker := NewHandshaker(stateDB, state, store, genDoc) - proxyApp := proxy.NewAppConns(clientCreator2) + proxyApp = proxy.NewAppConns(clientCreator2) if err := proxyApp.Start(); err != nil { t.Fatalf("Error starting proxy app connections: %v", err) } @@ -391,8 +403,8 @@ func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uin t.Fatalf("Expected app hashes to match after handshake/replay. got %X, expected %X", res.LastBlockAppHash, latestAppHash) } - expectedBlocksToSync := NUM_BLOCKS - nBlocks - if nBlocks == NUM_BLOCKS && mode > 0 { + expectedBlocksToSync := numBlocks - nBlocks + if nBlocks == numBlocks && mode > 0 { expectedBlocksToSync++ } else if nBlocks > 0 && mode == 1 { expectedBlocksToSync++ @@ -407,7 +419,7 @@ func applyBlock(stateDB dbm.DB, st sm.State, blk *types.Block, proxyApp proxy.Ap testPartSize := types.BlockPartSizeBytes blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool) - blkID := types.BlockID{blk.Hash(), blk.MakePartSet(testPartSize).Header()} + blkID := types.BlockID{Hash: blk.Hash(), PartsHeader: blk.MakePartSet(testPartSize).Header()} newState, err := blockExec.ApplyBlock(st, blkID, blk) if err != nil { panic(err) @@ -451,14 +463,8 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, stateDB dbm.DB, } -func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, chain []*types.Block, mode uint) sm.State { - // run the whole chain against this client to build up the tendermint state - clientCreator := proxy.NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(path.Join(config.DBDir(), "1"))) - proxyApp := proxy.NewAppConns(clientCreator) - if err := proxyApp.Start(); err != nil { - panic(err) - } - defer proxyApp.Stop() +func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, + chain []*types.Block, proxyApp proxy.AppConns, mode uint) sm.State { validators := types.TM2PB.ValidatorUpdates(state.Validators) if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{ @@ -489,28 +495,162 @@ func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, c return state } +func TestHandshakePanicsIfAppReturnsWrongAppHash(t *testing.T) { + // 1. Initialize tendermint and commit 3 blocks with the following app hashes: + // - 0x01 + // - 0x02 + // - 0x03 + config := ResetConfig("handshake_test_") + defer os.RemoveAll(config.RootDir) + privVal := privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) + const appVersion = 0x0 + stateDB, state, store := stateAndStore(config, privVal.GetPubKey(), appVersion) + genDoc, _ := sm.MakeGenesisDocFromFile(config.GenesisFile()) + state.LastValidators = state.Validators.Copy() + // mode = 0 for committing all the blocks + blocks := makeBlocks(3, &state, privVal) + store.chain = blocks + + // 2. Tendermint must panic if app returns wrong hash for the first block + // - RANDOM HASH + // - 0x02 + // - 0x03 + { + app := &badApp{numBlocks: 3, allHashesAreWrong: true} + clientCreator := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(clientCreator) + err := proxyApp.Start() + require.NoError(t, err) + defer proxyApp.Stop() + + assert.Panics(t, func() { + h := NewHandshaker(stateDB, state, store, genDoc) + h.Handshake(proxyApp) + }) + } + + // 3. Tendermint must panic if app returns wrong hash for the last block + // - 0x01 + // - 0x02 + // - RANDOM HASH + { + app := &badApp{numBlocks: 3, onlyLastHashIsWrong: true} + clientCreator := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(clientCreator) + err := proxyApp.Start() + require.NoError(t, err) + defer proxyApp.Stop() + + assert.Panics(t, func() { + h := NewHandshaker(stateDB, state, store, genDoc) + h.Handshake(proxyApp) + }) + } +} + +func makeBlocks(n int, state *sm.State, privVal types.PrivValidator) []*types.Block { + blocks := make([]*types.Block, 0) + + var ( + prevBlock *types.Block + prevBlockMeta *types.BlockMeta + ) + + appHeight := byte(0x01) + for i := 0; i < n; i++ { + height := int64(i + 1) + + block, parts := makeBlock(*state, prevBlock, prevBlockMeta, privVal, height) + blocks = append(blocks, block) + + prevBlock = block + prevBlockMeta = types.NewBlockMeta(block, parts) + + // update state + state.AppHash = []byte{appHeight} + appHeight++ + state.LastBlockHeight = height + } + + return blocks +} + +func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { + addr := privVal.GetPubKey().Address() + idx, _ := valset.GetByAddress(addr) + vote := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: header.Height, + Round: 1, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + + privVal.SignVote(header.ChainID, vote) + + return vote +} + +func makeBlock(state sm.State, lastBlock *types.Block, lastBlockMeta *types.BlockMeta, + privVal types.PrivValidator, height int64) (*types.Block, *types.PartSet) { + + lastCommit := types.NewCommit(types.BlockID{}, nil) + if height > 1 { + vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVal).CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + } + + return state.MakeBlock(height, []types.Tx{}, lastCommit, nil, state.Validators.GetProposer().Address) +} + +type badApp struct { + abci.BaseApplication + numBlocks byte + height byte + allHashesAreWrong bool + onlyLastHashIsWrong bool +} + +func (app *badApp) Commit() abci.ResponseCommit { + app.height++ + if app.onlyLastHashIsWrong { + if app.height == app.numBlocks { + return abci.ResponseCommit{Data: cmn.RandBytes(8)} + } + return abci.ResponseCommit{Data: []byte{app.height}} + } else if app.allHashesAreWrong { + return abci.ResponseCommit{Data: cmn.RandBytes(8)} + } + + panic("either allHashesAreWrong or onlyLastHashIsWrong must be set") +} + //-------------------------- // utils for making blocks func makeBlockchainFromWAL(wal WAL) ([]*types.Block, []*types.Commit, error) { + var height int64 + // Search for height marker - gr, found, err := wal.SearchForEndHeight(0, &WALSearchOptions{}) + gr, found, err := wal.SearchForEndHeight(height, &WALSearchOptions{}) if err != nil { return nil, nil, err } if !found { - return nil, nil, fmt.Errorf("WAL does not contain height %d.", 1) + return nil, nil, fmt.Errorf("WAL does not contain height %d", height) } defer gr.Close() // nolint: errcheck // log.Notice("Build a blockchain by reading from the WAL") - var blocks []*types.Block - var commits []*types.Commit - - var thisBlockParts *types.PartSet - var thisBlockCommit *types.Commit - var height int64 + var ( + blocks []*types.Block + commits []*types.Commit + thisBlockParts *types.PartSet + thisBlockCommit *types.Commit + ) dec := NewWALDecoder(gr) for { @@ -602,7 +742,7 @@ func stateAndStore(config *cfg.Config, pubKey crypto.PubKey, appVersion version. stateDB := dbm.NewMemDB() state, _ := sm.MakeGenesisStateFromFile(config.GenesisFile()) state.Version.Consensus.App = appVersion - store := NewMockBlockStore(config, state.ConsensusParams) + store := newMockBlockStore(config, state.ConsensusParams) return stateDB, state, store } @@ -617,7 +757,7 @@ type mockBlockStore struct { } // TODO: NewBlockStore(db.NewMemDB) ... -func NewMockBlockStore(config *cfg.Config, params types.ConsensusParams) *mockBlockStore { +func newMockBlockStore(config *cfg.Config, params types.ConsensusParams) *mockBlockStore { return &mockBlockStore{config, params, nil, nil} } @@ -626,7 +766,7 @@ func (bs *mockBlockStore) LoadBlock(height int64) *types.Block { return bs.chain func (bs *mockBlockStore) LoadBlockMeta(height int64) *types.BlockMeta { block := bs.chain[height-1] return &types.BlockMeta{ - BlockID: types.BlockID{block.Hash(), block.MakePartSet(types.BlockPartSizeBytes).Header()}, + BlockID: types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(types.BlockPartSizeBytes).Header()}, Header: block.Header, } } @@ -640,15 +780,16 @@ func (bs *mockBlockStore) LoadSeenCommit(height int64) *types.Commit { return bs.commits[height-1] } -//---------------------------------------- +//--------------------------------------- +// Test handshake/init chain -func TestInitChainUpdateValidators(t *testing.T) { +func TestHandshakeUpdatesValidators(t *testing.T) { val, _ := types.RandValidator(true, 10) vals := types.NewValidatorSet([]*types.Validator{val}) app := &initChainApp{vals: types.TM2PB.ValidatorUpdates(vals)} clientCreator := proxy.NewLocalClientCreator(app) - config := ResetConfig("proxy_test_") + config := ResetConfig("handshake_test_") defer os.RemoveAll(config.RootDir) privVal := privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) stateDB, state, store := stateAndStore(config, privVal.GetPubKey(), 0x0) From 968e955c46e08a58e4319f9e3dedbe2e91317e8f Mon Sep 17 00:00:00 2001 From: Greg Hill Date: Tue, 23 Apr 2019 09:52:46 +0100 Subject: [PATCH 008/211] testnet cmd: add config option (#3559) Option to explicitly provide the -config in the testnet command, closing #3160. --- CHANGELOG_PENDING.md | 1 + cmd/tendermint/commands/testnet.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index e05430218..bfad14700 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -18,6 +18,7 @@ ### IMPROVEMENTS: - [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC +- [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `-config=` option to `testnet` cmd (@gregdhill) - [cs/replay] \#3460 check appHash for each block ### BUG FIXES: diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index e34b8d305..b4b33e655 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/spf13/cobra" + "github.com/spf13/viper" cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" @@ -20,6 +21,7 @@ import ( var ( nValidators int nNonValidators int + configFile string outputDir string nodeDirPrefix string @@ -36,6 +38,8 @@ const ( func init() { TestnetFilesCmd.Flags().IntVar(&nValidators, "v", 4, "Number of validators to initialize the testnet with") + TestnetFilesCmd.Flags().StringVar(&configFile, "config", "", + "Config file to use (note some options may be overwritten)") TestnetFilesCmd.Flags().IntVar(&nNonValidators, "n", 0, "Number of non-validators to initialize the testnet with") TestnetFilesCmd.Flags().StringVar(&outputDir, "o", "./mytestnet", @@ -73,6 +77,21 @@ Example: func testnetFiles(cmd *cobra.Command, args []string) error { config := cfg.DefaultConfig() + + // overwrite default config if set and valid + if configFile != "" { + viper.SetConfigFile(configFile) + if err := viper.ReadInConfig(); err != nil { + return err + } + if err := viper.Unmarshal(config); err != nil { + return err + } + if err := config.ValidateBasic(); err != nil { + return err + } + } + genVals := make([]types.GenesisValidator, nValidators) for i := 0; i < nValidators; i++ { From b738add80cb9f7e7e91cbad38ec6db297f3535e6 Mon Sep 17 00:00:00 2001 From: Zarko Milosevic Date: Tue, 23 Apr 2019 11:19:16 +0200 Subject: [PATCH 009/211] cs: fix nondeterministic tests (#3582) Should fix #3451, #2723 and #3317. Test TestResetTimeoutPrecommitUponNewHeight is simplified so it reduces a risk of timeout failure. Furthermore, timeout we wait for TimeoutEvents is increased, and the timeout value is more precisely computed. This should hopefully decrease a chance of non-deterministic test failures. This assertion is problematic to ensure consistently due to dependency on scheduler. On the other hand, if I am not wrong, order in which messages are read from the channel respects order in which messages are written. Therefore, process will receive 2f+1 precommits that are not all for v (one is for nil) so TriggeredTimeoutPrecommit would be set to true. So we don't need to assert it. I know that it would be better to still assert to it but I don't know how to do it without sleep and that is ugly and is causing us nondeterministic failures. --- CHANGELOG_PENDING.md | 1 + consensus/common_test.go | 4 +-- consensus/state_test.go | 54 +++++++++++++++++++--------------------- 3 files changed, 28 insertions(+), 31 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index bfad14700..2d2830094 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -25,3 +25,4 @@ - [state] [\#3537](https://github.com/tendermint/tendermint/pull/3537#issuecomment-482711833) LoadValidators: do not return an empty validator set - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode to 16 (as a result, the node will stop retrying after a 35 hours time window) +- [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests \ No newline at end of file diff --git a/consensus/common_test.go b/consensus/common_test.go index 5706f2317..8f305139e 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -351,7 +351,7 @@ func ensureNoNewUnlock(unlockCh <-chan tmpubsub.Message) { } func ensureNoNewTimeout(stepCh <-chan tmpubsub.Message, timeout int64) { - timeoutDuration := time.Duration(timeout*5) * time.Nanosecond + timeoutDuration := time.Duration(timeout*10) * time.Nanosecond ensureNoNewEvent( stepCh, timeoutDuration, @@ -398,7 +398,7 @@ func ensureNewRound(roundCh <-chan tmpubsub.Message, height int64, round int) { } func ensureNewTimeout(timeoutCh <-chan tmpubsub.Message, height int64, round int, timeout int64) { - timeoutDuration := time.Duration(timeout*5) * time.Nanosecond + timeoutDuration := time.Duration(timeout*10) * time.Nanosecond ensureNewEvent(timeoutCh, height, round, timeoutDuration, "Timeout expired while waiting for NewTimeout event") } diff --git a/consensus/state_test.go b/consensus/state_test.go index fc1e3e949..67b5cfbdc 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -370,7 +370,7 @@ func TestStateLockNoPOL(t *testing.T) { // (note we're entering precommit for a second time this round) // but with invalid args. then we enterPrecommitWait, and the timeout to new round - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) /// @@ -384,7 +384,7 @@ func TestStateLockNoPOL(t *testing.T) { incrementRound(vs2) // now we're on a new round and not the proposer, so wait for timeout - ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.TimeoutPropose.Nanoseconds()) + ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds()) rs := cs1.GetRoundState() @@ -403,7 +403,7 @@ func TestStateLockNoPOL(t *testing.T) { // now we're going to enter prevote again, but with invalid args // and then prevote wait, which should timeout. then wait for precommit - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrevote.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Prevote(round).Nanoseconds()) ensurePrecommit(voteCh, height, round) // precommit // the proposed block should still be locked and our precommit added @@ -416,7 +416,7 @@ func TestStateLockNoPOL(t *testing.T) { // (note we're entering precommit for a second time this round, but with invalid args // then we enterPrecommitWait and timeout into NewRound - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) round = round + 1 // entering new round ensureNewRound(newRoundCh, height, round) @@ -441,7 +441,7 @@ func TestStateLockNoPOL(t *testing.T) { signAddVotes(cs1, types.PrevoteType, hash, rs.ProposalBlock.MakePartSet(partSize).Header(), vs2) ensurePrevote(voteCh, height, round) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrevote.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Prevote(round).Nanoseconds()) ensurePrecommit(voteCh, height, round) // precommit validatePrecommit(t, cs1, round, 0, vss[0], nil, theBlockHash) // precommit nil but be locked on proposal @@ -449,7 +449,7 @@ func TestStateLockNoPOL(t *testing.T) { signAddVotes(cs1, types.PrecommitType, hash, rs.ProposalBlock.MakePartSet(partSize).Header(), vs2) // NOTE: conflicting precommits at same height ensurePrecommit(voteCh, height, round) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) cs2, _ := randConsensusState(2) // needed so generated block is different than locked block // before we time out into new round, set next proposal block @@ -482,7 +482,7 @@ func TestStateLockNoPOL(t *testing.T) { signAddVotes(cs1, types.PrevoteType, propBlock.Hash(), propBlock.MakePartSet(partSize).Header(), vs2) ensurePrevote(voteCh, height, round) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrevote.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Prevote(round).Nanoseconds()) ensurePrecommit(voteCh, height, round) validatePrecommit(t, cs1, round, 0, vss[0], nil, theBlockHash) // precommit nil but locked on proposal @@ -542,7 +542,7 @@ func TestStateLockPOLRelock(t *testing.T) { incrementRound(vs2, vs3, vs4) // timeout to new round - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) round = round + 1 // moving to the next round //XXX: this isnt guaranteed to get there before the timeoutPropose ... @@ -632,7 +632,7 @@ func TestStateLockPOLUnlock(t *testing.T) { propBlockParts := propBlock.MakePartSet(partSize) // timeout to new round - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) rs = cs1.GetRoundState() lockedBlockHash := rs.LockedBlock.Hash() @@ -710,7 +710,7 @@ func TestStateLockPOLSafety1(t *testing.T) { // cs1 precommit nil ensurePrecommit(voteCh, height, round) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) t.Log("### ONTO ROUND 1") @@ -754,7 +754,7 @@ func TestStateLockPOLSafety1(t *testing.T) { signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) incrementRound(vs2, vs3, vs4) round = round + 1 // moving to the next round @@ -767,7 +767,7 @@ func TestStateLockPOLSafety1(t *testing.T) { */ // timeout of propose - ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.TimeoutPropose.Nanoseconds()) + ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds()) // finish prevote ensurePrevote(voteCh, height, round) @@ -850,7 +850,7 @@ func TestStateLockPOLSafety2(t *testing.T) { incrementRound(vs2, vs3, vs4) // timeout of precommit wait to new round - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) round = round + 1 // moving to the next round // in round 2 we see the polkad block from round 0 @@ -919,7 +919,7 @@ func TestProposeValidBlock(t *testing.T) { signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) incrementRound(vs2, vs3, vs4) round = round + 1 // moving to the next round @@ -929,7 +929,7 @@ func TestProposeValidBlock(t *testing.T) { t.Log("### ONTO ROUND 2") // timeout of propose - ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.TimeoutPropose.Nanoseconds()) + ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds()) ensurePrevote(voteCh, height, round) validatePrevote(t, cs1, round, vss[0], propBlockHash) @@ -952,7 +952,7 @@ func TestProposeValidBlock(t *testing.T) { ensureNewRound(newRoundCh, height, round) t.Log("### ONTO ROUND 3") - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) round = round + 1 // moving to the next round @@ -1004,7 +1004,7 @@ func TestSetValidBlockOnDelayedPrevote(t *testing.T) { // vs3 send prevote nil signAddVotes(cs1, types.PrevoteType, nil, types.PartSetHeader{}, vs3) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrevote.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Prevote(round).Nanoseconds()) ensurePrecommit(voteCh, height, round) // we should have precommitted @@ -1052,7 +1052,7 @@ func TestSetValidBlockOnDelayedProposal(t *testing.T) { startTestRound(cs1, cs1.Height, round) ensureNewRound(newRoundCh, height, round) - ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.TimeoutPropose.Nanoseconds()) + ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds()) ensurePrevote(voteCh, height, round) validatePrevote(t, cs1, round, vss[0], nil) @@ -1065,7 +1065,7 @@ func TestSetValidBlockOnDelayedProposal(t *testing.T) { signAddVotes(cs1, types.PrevoteType, propBlockHash, propBlockParts.Header(), vs2, vs3, vs4) ensureNewValidBlock(validBlockCh, height, round) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrevote.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Prevote(round).Nanoseconds()) ensurePrecommit(voteCh, height, round) validatePrecommit(t, cs1, round, -1, vss[0], nil, nil) @@ -1099,7 +1099,7 @@ func TestWaitingTimeoutOnNilPolka(t *testing.T) { signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) ensureNewRound(newRoundCh, height, round+1) } @@ -1131,7 +1131,7 @@ func TestWaitingTimeoutProposeOnNewRound(t *testing.T) { rs := cs1.GetRoundState() assert.True(t, rs.Step == cstypes.RoundStepPropose) // P0 does not prevote before timeoutPropose expires - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPropose.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Propose(round).Nanoseconds()) ensurePrevote(voteCh, height, round) validatePrevote(t, cs1, round, vss[0], nil) @@ -1165,7 +1165,7 @@ func TestRoundSkipOnNilPolkaFromHigherRound(t *testing.T) { ensurePrecommit(voteCh, height, round) validatePrecommit(t, cs1, round, -1, vss[0], nil, nil) - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) round = round + 1 // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -1191,7 +1191,7 @@ func TestWaitTimeoutProposeOnNilPolkaForTheCurrentRound(t *testing.T) { incrementRound(vss[1:]...) signAddVotes(cs1, types.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.TimeoutPropose.Nanoseconds()) + ensureNewTimeout(timeoutProposeCh, height, round, cs1.config.Propose(round).Nanoseconds()) ensurePrevote(voteCh, height, round) validatePrevote(t, cs1, round, vss[0], nil) @@ -1332,7 +1332,7 @@ func TestStartNextHeightCorrectly(t *testing.T) { cs1.txNotifier.(*fakeTxNotifier).Notify() - ensureNewTimeout(timeoutProposeCh, height+1, round, cs1.config.TimeoutPropose.Nanoseconds()) + ensureNewTimeout(timeoutProposeCh, height+1, round, cs1.config.Propose(round).Nanoseconds()) rs = cs1.GetRoundState() assert.False(t, rs.TriggeredTimeoutPrecommit, "triggeredTimeoutPrecommit should be false at the beginning of each round") } @@ -1375,12 +1375,8 @@ func TestResetTimeoutPrecommitUponNewHeight(t *testing.T) { // add precommits signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs3) - time.Sleep(5 * time.Millisecond) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs4) - rs = cs1.GetRoundState() - assert.True(t, rs.TriggeredTimeoutPrecommit) - ensureNewBlockHeader(newBlockHeader, height, theBlockHash) prop, propBlock := decideProposal(cs1, vs2, height+1, 0) @@ -1519,7 +1515,7 @@ func TestStateHalt1(t *testing.T) { incrementRound(vs2, vs3, vs4) // timeout to new round - ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.TimeoutPrecommit.Nanoseconds()) + ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) round = round + 1 // moving to the next round From 90997ab1b56d2acbd022d7f1d7923873d6bc2e3a Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 23 Apr 2019 05:34:14 -0400 Subject: [PATCH 010/211] docs: update contributing.md (#3503) Minor updates to reflect squash merging and how to prepare releases --- CONTRIBUTING.md | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3dab3b8ab..f2320fb4c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,10 +105,14 @@ removed from the header in rpc responses as well. ## Branching Model and Release -All repos should adhere to the branching model: http://nvie.com/posts/a-successful-git-branching-model/. +We follow a variant of [git flow](http://nvie.com/posts/a-successful-git-branching-model/). This means that all pull-requests should be made against develop. Any merge to master constitutes a tagged release. +Note all pull requests should be squash merged except for merging to master and +merging master back to develop. This keeps the commit history clean and makes it +easy to reference the pull request where a change was introduced. + ### Development Procedure: - the latest state of development is on `develop` - `develop` must never fail `make test` @@ -120,13 +124,13 @@ master constitutes a tagged release. ### Pull Merge Procedure: - ensure pull branch is based on a recent develop - run `make test` to ensure that all tests pass -- merge pull request +- squash merge pull request - the `unstable` branch may be used to aggregate pull merges before fixing tests ### Release Procedure: - start on `develop` - run integration tests (see `test_integrations` in Makefile) -- prepare changelog: +- prepare release in a pull request against develop (to be squash merged): - copy `CHANGELOG_PENDING.md` to top of `CHANGELOG.md` - run `python ./scripts/linkify_changelog.py CHANGELOG.md` to add links for all issues @@ -135,23 +139,15 @@ master constitutes a tagged release. the changelog. To lookup an alias from an email, try `bash ./scripts/authors.sh ` - reset the `CHANGELOG_PENDING.md` -- bump versions -- push to release/vX.X.X to run the extended integration tests on the CI -- merge to master -- merge master back to develop + - bump versions +- push latest develop with prepared release details to release/vX.X.X to run the extended integration tests on the CI +- if necessary, make pull requests against release/vX.X.X and squash merge them +- merge to master (don't squash merge!) +- merge master back to develop (don't squash merge!) ### Hotfix Procedure: -- start on `master` -- checkout a new branch named hotfix-vX.X.X -- make the required changes - - these changes should be small and an absolute necessity - - add a note to CHANGELOG.md -- bump versions -- push to hotfix-vX.X.X to run the extended integration tests on the CI -- merge hotfix-vX.X.X to master -- merge hotfix-vX.X.X to develop -- delete the hotfix-vX.X.X branch +- follow the normal development and release procedure without any differences ## Testing From 70592cc4d87af013db4358186c484e64ee974bb6 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 26 Apr 2019 06:23:43 -0400 Subject: [PATCH 011/211] libs/common: remove deprecated PanicXXX functions (#3595) * Remove deprecated PanicXXX functions from codebase As per discussion over [here](https://github.com/tendermint/tendermint/pull/3456#discussion_r278423492), we need to remove these `PanicXXX` functions and eliminate our dependence on them. In this PR, each and every `PanicXXX` function call is replaced with a simple `panic` call. * add a changelog entry --- CHANGELOG_PENDING.md | 3 ++- blockchain/store.go | 10 ++++----- config/toml.go | 6 ++--- consensus/reactor.go | 4 ++-- consensus/state.go | 30 ++++++++++++------------- consensus/types/height_vote_set.go | 8 +++---- crypto/xsalsa20symmetric/symmetric.go | 5 ++--- libs/common/errors.go | 32 --------------------------- libs/common/random.go | 2 +- libs/common/service.go | 3 +-- libs/db/go_level_db.go | 10 ++++----- node/node.go | 4 ++-- p2p/conn/connection.go | 2 +- p2p/netaddress.go | 4 +--- p2p/pex/file.go | 4 ++-- p2p/switch.go | 2 +- p2p/trust/store.go | 2 +- types/part_set.go | 2 +- types/validator.go | 3 +-- types/vote.go | 2 +- types/vote_set.go | 16 +++++++------- 21 files changed, 57 insertions(+), 97 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 2d2830094..6065ee9b8 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -9,6 +9,7 @@ * Apps * Go API +- [libs/common] Removed PanicSanity, PanicCrisis, PanicConsensus and PanicQ * Blockchain Protocol @@ -25,4 +26,4 @@ - [state] [\#3537](https://github.com/tendermint/tendermint/pull/3537#issuecomment-482711833) LoadValidators: do not return an empty validator set - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode to 16 (as a result, the node will stop retrying after a 35 hours time window) -- [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests \ No newline at end of file +- [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests diff --git a/blockchain/store.go b/blockchain/store.go index 498cca68d..b7f4e07c8 100644 --- a/blockchain/store.go +++ b/blockchain/store.go @@ -144,14 +144,14 @@ func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit { // most recent height. Otherwise they'd stall at H-1. func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, seenCommit *types.Commit) { if block == nil { - cmn.PanicSanity("BlockStore can only save a non-nil block") + panic("BlockStore can only save a non-nil block") } height := block.Height if g, w := height, bs.Height()+1; g != w { - cmn.PanicSanity(fmt.Sprintf("BlockStore can only save contiguous blocks. Wanted %v, got %v", w, g)) + panic(fmt.Sprintf("BlockStore can only save contiguous blocks. Wanted %v, got %v", w, g)) } if !blockParts.IsComplete() { - cmn.PanicSanity(fmt.Sprintf("BlockStore can only save complete block part sets")) + panic(fmt.Sprintf("BlockStore can only save complete block part sets")) } // Save block meta @@ -188,7 +188,7 @@ func (bs *BlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet, s func (bs *BlockStore) saveBlockPart(height int64, index int, part *types.Part) { if height != bs.Height()+1 { - cmn.PanicSanity(fmt.Sprintf("BlockStore can only save contiguous blocks. Wanted %v, got %v", bs.Height()+1, height)) + panic(fmt.Sprintf("BlockStore can only save contiguous blocks. Wanted %v, got %v", bs.Height()+1, height)) } partBytes := cdc.MustMarshalBinaryBare(part) bs.db.Set(calcBlockPartKey(height, index), partBytes) @@ -224,7 +224,7 @@ type BlockStoreStateJSON struct { func (bsj BlockStoreStateJSON) Save(db dbm.DB) { bytes, err := cdc.MarshalJSON(bsj) if err != nil { - cmn.PanicSanity(fmt.Sprintf("Could not marshal state bytes: %v", err)) + panic(fmt.Sprintf("Could not marshal state bytes: %v", err)) } db.SetSync(blockStoreKey, bytes) } diff --git a/config/toml.go b/config/toml.go index 978255aba..f80f525e4 100644 --- a/config/toml.go +++ b/config/toml.go @@ -28,13 +28,13 @@ func init() { // and panics if it fails. func EnsureRoot(rootDir string) { if err := cmn.EnsureDir(rootDir, DefaultDirPerm); err != nil { - cmn.PanicSanity(err.Error()) + panic(err.Error()) } if err := cmn.EnsureDir(filepath.Join(rootDir, defaultConfigDir), DefaultDirPerm); err != nil { - cmn.PanicSanity(err.Error()) + panic(err.Error()) } if err := cmn.EnsureDir(filepath.Join(rootDir, defaultDataDir), DefaultDirPerm); err != nil { - cmn.PanicSanity(err.Error()) + panic(err.Error()) } configFilePath := filepath.Join(rootDir, defaultConfigFilePath) diff --git a/consensus/reactor.go b/consensus/reactor.go index 604e54b49..937a12351 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -491,7 +491,7 @@ OUTER_LOOP: if prs.ProposalBlockParts == nil { blockMeta := conR.conS.blockStore.LoadBlockMeta(prs.Height) if blockMeta == nil { - cmn.PanicCrisis(fmt.Sprintf("Failed to load block %d when blockStore is at %d", + panic(fmt.Sprintf("Failed to load block %d when blockStore is at %d", prs.Height, conR.conS.blockStore.Height())) } ps.InitProposalBlockParts(blockMeta.BlockID.PartsHeader) @@ -1110,7 +1110,7 @@ func (ps *PeerState) ensureCatchupCommitRound(height int64, round int, numValida NOTE: This is wrong, 'round' could change. e.g. if orig round is not the same as block LastCommit round. if ps.CatchupCommitRound != -1 && ps.CatchupCommitRound != round { - cmn.PanicSanity(fmt.Sprintf("Conflicting CatchupCommitRound. Height: %v, Orig: %v, New: %v", height, ps.CatchupCommitRound, round)) + panic(fmt.Sprintf("Conflicting CatchupCommitRound. Height: %v, Orig: %v, New: %v", height, ps.CatchupCommitRound, round)) } */ if ps.PRS.CatchupCommitRound == round { diff --git a/consensus/state.go b/consensus/state.go index 74ec092ff..a6256f82d 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -491,11 +491,11 @@ func (cs *ConsensusState) reconstructLastCommit(state sm.State) { } added, err := lastPrecommits.AddVote(seenCommit.ToVote(precommit)) if !added || err != nil { - cmn.PanicCrisis(fmt.Sprintf("Failed to reconstruct LastCommit: %v", err)) + panic(fmt.Sprintf("Failed to reconstruct LastCommit: %v", err)) } } if !lastPrecommits.HasTwoThirdsMajority() { - cmn.PanicSanity("Failed to reconstruct LastCommit: Does not have +2/3 maj") + panic("Failed to reconstruct LastCommit: Does not have +2/3 maj") } cs.LastCommit = lastPrecommits } @@ -504,13 +504,13 @@ func (cs *ConsensusState) reconstructLastCommit(state sm.State) { // The round becomes 0 and cs.Step becomes cstypes.RoundStepNewHeight. func (cs *ConsensusState) updateToState(state sm.State) { if cs.CommitRound > -1 && 0 < cs.Height && cs.Height != state.LastBlockHeight { - cmn.PanicSanity(fmt.Sprintf("updateToState() expected state height of %v but found %v", + panic(fmt.Sprintf("updateToState() expected state height of %v but found %v", cs.Height, state.LastBlockHeight)) } if !cs.state.IsEmpty() && cs.state.LastBlockHeight+1 != cs.Height { // This might happen when someone else is mutating cs.state. // Someone forgot to pass in state.Copy() somewhere?! - cmn.PanicSanity(fmt.Sprintf("Inconsistent cs.state.LastBlockHeight+1 %v vs cs.Height %v", + panic(fmt.Sprintf("Inconsistent cs.state.LastBlockHeight+1 %v vs cs.Height %v", cs.state.LastBlockHeight+1, cs.Height)) } @@ -530,7 +530,7 @@ func (cs *ConsensusState) updateToState(state sm.State) { lastPrecommits := (*types.VoteSet)(nil) if cs.CommitRound > -1 && cs.Votes != nil { if !cs.Votes.Precommits(cs.CommitRound).HasTwoThirdsMajority() { - cmn.PanicSanity("updateToState(state) called but last Precommit round didn't have +2/3") + panic("updateToState(state) called but last Precommit round didn't have +2/3") } lastPrecommits = cs.Votes.Precommits(cs.CommitRound) } @@ -1047,7 +1047,7 @@ func (cs *ConsensusState) enterPrevoteWait(height int64, round int) { return } if !cs.Votes.Prevotes(round).HasTwoThirdsAny() { - cmn.PanicSanity(fmt.Sprintf("enterPrevoteWait(%v/%v), but Prevotes does not have any +2/3 votes", height, round)) + panic(fmt.Sprintf("enterPrevoteWait(%v/%v), but Prevotes does not have any +2/3 votes", height, round)) } logger.Info(fmt.Sprintf("enterPrevoteWait(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) @@ -1103,7 +1103,7 @@ func (cs *ConsensusState) enterPrecommit(height int64, round int) { // the latest POLRound should be this round. polRound, _ := cs.Votes.POLInfo() if polRound < round { - cmn.PanicSanity(fmt.Sprintf("This POLRound should be %v but got %v", round, polRound)) + panic(fmt.Sprintf("This POLRound should be %v but got %v", round, polRound)) } // +2/3 prevoted nil. Unlock and precommit nil. @@ -1137,7 +1137,7 @@ func (cs *ConsensusState) enterPrecommit(height int64, round int) { logger.Info("enterPrecommit: +2/3 prevoted proposal block. Locking", "hash", blockID.Hash) // Validate the block. if err := cs.blockExec.ValidateBlock(cs.state, cs.ProposalBlock); err != nil { - cmn.PanicConsensus(fmt.Sprintf("enterPrecommit: +2/3 prevoted for an invalid block: %v", err)) + panic(fmt.Sprintf("enterPrecommit: +2/3 prevoted for an invalid block: %v", err)) } cs.LockedRound = round cs.LockedBlock = cs.ProposalBlock @@ -1175,7 +1175,7 @@ func (cs *ConsensusState) enterPrecommitWait(height int64, round int) { return } if !cs.Votes.Precommits(round).HasTwoThirdsAny() { - cmn.PanicSanity(fmt.Sprintf("enterPrecommitWait(%v/%v), but Precommits does not have any +2/3 votes", height, round)) + panic(fmt.Sprintf("enterPrecommitWait(%v/%v), but Precommits does not have any +2/3 votes", height, round)) } logger.Info(fmt.Sprintf("enterPrecommitWait(%v/%v). Current: %v/%v/%v", height, round, cs.Height, cs.Round, cs.Step)) @@ -1214,7 +1214,7 @@ func (cs *ConsensusState) enterCommit(height int64, commitRound int) { blockID, ok := cs.Votes.Precommits(commitRound).TwoThirdsMajority() if !ok { - cmn.PanicSanity("RunActionCommit() expects +2/3 precommits") + panic("RunActionCommit() expects +2/3 precommits") } // The Locked* fields no longer matter. @@ -1247,7 +1247,7 @@ func (cs *ConsensusState) tryFinalizeCommit(height int64) { logger := cs.Logger.With("height", height) if cs.Height != height { - cmn.PanicSanity(fmt.Sprintf("tryFinalizeCommit() cs.Height: %v vs height: %v", cs.Height, height)) + panic(fmt.Sprintf("tryFinalizeCommit() cs.Height: %v vs height: %v", cs.Height, height)) } blockID, ok := cs.Votes.Precommits(cs.CommitRound).TwoThirdsMajority() @@ -1277,16 +1277,16 @@ func (cs *ConsensusState) finalizeCommit(height int64) { block, blockParts := cs.ProposalBlock, cs.ProposalBlockParts if !ok { - cmn.PanicSanity(fmt.Sprintf("Cannot finalizeCommit, commit does not have two thirds majority")) + panic(fmt.Sprintf("Cannot finalizeCommit, commit does not have two thirds majority")) } if !blockParts.HasHeader(blockID.PartsHeader) { - cmn.PanicSanity(fmt.Sprintf("Expected ProposalBlockParts header to be commit header")) + panic(fmt.Sprintf("Expected ProposalBlockParts header to be commit header")) } if !block.HashesTo(blockID.Hash) { - cmn.PanicSanity(fmt.Sprintf("Cannot finalizeCommit, ProposalBlock does not hash to commit hash")) + panic(fmt.Sprintf("Cannot finalizeCommit, ProposalBlock does not hash to commit hash")) } if err := cs.blockExec.ValidateBlock(cs.state, block); err != nil { - cmn.PanicConsensus(fmt.Sprintf("+2/3 committed an invalid block: %v", err)) + panic(fmt.Sprintf("+2/3 committed an invalid block: %v", err)) } cs.Logger.Info(fmt.Sprintf("Finalizing commit of block with %d txs", block.NumTxs), diff --git a/consensus/types/height_vote_set.go b/consensus/types/height_vote_set.go index eee013eea..35c9a486d 100644 --- a/consensus/types/height_vote_set.go +++ b/consensus/types/height_vote_set.go @@ -6,7 +6,6 @@ import ( "strings" "sync" - cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/types" ) @@ -83,7 +82,7 @@ func (hvs *HeightVoteSet) SetRound(round int) { hvs.mtx.Lock() defer hvs.mtx.Unlock() if hvs.round != 0 && (round < hvs.round+1) { - cmn.PanicSanity("SetRound() must increment hvs.round") + panic("SetRound() must increment hvs.round") } for r := hvs.round + 1; r <= round; r++ { if _, ok := hvs.roundVoteSets[r]; ok { @@ -96,7 +95,7 @@ func (hvs *HeightVoteSet) SetRound(round int) { func (hvs *HeightVoteSet) addRound(round int) { if _, ok := hvs.roundVoteSets[round]; ok { - cmn.PanicSanity("addRound() for an existing round") + panic("addRound() for an existing round") } // log.Debug("addRound(round)", "round", round) prevotes := types.NewVoteSet(hvs.chainID, hvs.height, round, types.PrevoteType, hvs.valSet) @@ -169,8 +168,7 @@ func (hvs *HeightVoteSet) getVoteSet(round int, type_ types.SignedMsgType) *type case types.PrecommitType: return rvs.Precommits default: - cmn.PanicSanity(fmt.Sprintf("Unexpected vote type %X", type_)) - return nil + panic(fmt.Sprintf("Unexpected vote type %X", type_)) } } diff --git a/crypto/xsalsa20symmetric/symmetric.go b/crypto/xsalsa20symmetric/symmetric.go index 10a0f6f33..73dc9dec5 100644 --- a/crypto/xsalsa20symmetric/symmetric.go +++ b/crypto/xsalsa20symmetric/symmetric.go @@ -7,7 +7,6 @@ import ( "golang.org/x/crypto/nacl/secretbox" "github.com/tendermint/tendermint/crypto" - cmn "github.com/tendermint/tendermint/libs/common" ) // TODO, make this into a struct that implements crypto.Symmetric. @@ -19,7 +18,7 @@ const secretLen = 32 // The ciphertext is (secretbox.Overhead + 24) bytes longer than the plaintext. func EncryptSymmetric(plaintext []byte, secret []byte) (ciphertext []byte) { if len(secret) != secretLen { - cmn.PanicSanity(fmt.Sprintf("Secret must be 32 bytes long, got len %v", len(secret))) + panic(fmt.Sprintf("Secret must be 32 bytes long, got len %v", len(secret))) } nonce := crypto.CRandBytes(nonceLen) nonceArr := [nonceLen]byte{} @@ -36,7 +35,7 @@ func EncryptSymmetric(plaintext []byte, secret []byte) (ciphertext []byte) { // The ciphertext is (secretbox.Overhead + 24) bytes longer than the plaintext. func DecryptSymmetric(ciphertext []byte, secret []byte) (plaintext []byte, err error) { if len(secret) != secretLen { - cmn.PanicSanity(fmt.Sprintf("Secret must be 32 bytes long, got len %v", len(secret))) + panic(fmt.Sprintf("Secret must be 32 bytes long, got len %v", len(secret))) } if len(ciphertext) <= secretbox.Overhead+nonceLen { return nil, errors.New("Ciphertext is too short") diff --git a/libs/common/errors.go b/libs/common/errors.go index 10e40ebd2..24af84267 100644 --- a/libs/common/errors.go +++ b/libs/common/errors.go @@ -212,35 +212,3 @@ func (fe FmtError) String() string { func (fe FmtError) Format() string { return fe.format } - -//---------------------------------------- -// Panic wrappers -// XXX DEPRECATED - -// A panic resulting from a sanity check means there is a programmer error -// and some guarantee is not satisfied. -// XXX DEPRECATED -func PanicSanity(v interface{}) { - panic(fmt.Sprintf("Panicked on a Sanity Check: %v", v)) -} - -// A panic here means something has gone horribly wrong, in the form of data corruption or -// failure of the operating system. In a correct/healthy system, these should never fire. -// If they do, it's indicative of a much more serious problem. -// XXX DEPRECATED -func PanicCrisis(v interface{}) { - panic(fmt.Sprintf("Panicked on a Crisis: %v", v)) -} - -// Indicates a failure of consensus. Someone was malicious or something has -// gone horribly wrong. These should really boot us into an "emergency-recover" mode -// XXX DEPRECATED -func PanicConsensus(v interface{}) { - panic(fmt.Sprintf("Panicked on a Consensus Failure: %v", v)) -} - -// For those times when we're not sure if we should panic -// XXX DEPRECATED -func PanicQ(v interface{}) { - panic(fmt.Sprintf("Panicked questionably: %v", v)) -} diff --git a/libs/common/random.go b/libs/common/random.go index 2de65945c..47e44d1c0 100644 --- a/libs/common/random.go +++ b/libs/common/random.go @@ -300,7 +300,7 @@ func cRandBytes(numBytes int) []byte { b := make([]byte, numBytes) _, err := crand.Read(b) if err != nil { - PanicCrisis(err) + panic(err) } return b } diff --git a/libs/common/service.go b/libs/common/service.go index 21fb0df3e..8eee48138 100644 --- a/libs/common/service.go +++ b/libs/common/service.go @@ -194,8 +194,7 @@ func (bs *BaseService) Reset() error { // OnReset implements Service by panicking. func (bs *BaseService) OnReset() error { - PanicSanity("The service cannot be reset") - return nil + panic("The service cannot be reset") } // IsRunning implements Service by returning true or false depending on the diff --git a/libs/db/go_level_db.go b/libs/db/go_level_db.go index 9a4358f60..79ee5ccb9 100644 --- a/libs/db/go_level_db.go +++ b/libs/db/go_level_db.go @@ -9,8 +9,6 @@ import ( "github.com/syndtr/goleveldb/leveldb/errors" "github.com/syndtr/goleveldb/leveldb/iterator" "github.com/syndtr/goleveldb/leveldb/opt" - - cmn "github.com/tendermint/tendermint/libs/common" ) func init() { @@ -67,7 +65,7 @@ func (db *GoLevelDB) Set(key []byte, value []byte) { value = nonNilBytes(value) err := db.db.Put(key, value, nil) if err != nil { - cmn.PanicCrisis(err) + panic(err) } } @@ -77,7 +75,7 @@ func (db *GoLevelDB) SetSync(key []byte, value []byte) { value = nonNilBytes(value) err := db.db.Put(key, value, &opt.WriteOptions{Sync: true}) if err != nil { - cmn.PanicCrisis(err) + panic(err) } } @@ -86,7 +84,7 @@ func (db *GoLevelDB) Delete(key []byte) { key = nonNilBytes(key) err := db.db.Delete(key, nil) if err != nil { - cmn.PanicCrisis(err) + panic(err) } } @@ -95,7 +93,7 @@ func (db *GoLevelDB) DeleteSync(key []byte) { key = nonNilBytes(key) err := db.db.Delete(key, &opt.WriteOptions{Sync: true}) if err != nil { - cmn.PanicCrisis(err) + panic(err) } } diff --git a/node/node.go b/node/node.go index c0a4d736e..9ad7172fb 100644 --- a/node/node.go +++ b/node/node.go @@ -912,7 +912,7 @@ func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) { var genDoc *types.GenesisDoc err := cdc.UnmarshalJSON(bytes, &genDoc) if err != nil { - cmn.PanicCrisis(fmt.Sprintf("Failed to load genesis doc due to unmarshaling error: %v (bytes: %X)", err, bytes)) + panic(fmt.Sprintf("Failed to load genesis doc due to unmarshaling error: %v (bytes: %X)", err, bytes)) } return genDoc, nil } @@ -921,7 +921,7 @@ func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) { func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) { bytes, err := cdc.MarshalJSON(genDoc) if err != nil { - cmn.PanicCrisis(fmt.Sprintf("Failed to save genesis doc due to marshaling error: %v", err)) + panic(fmt.Sprintf("Failed to save genesis doc due to marshaling error: %v", err)) } db.SetSync(genesisDocKey, bytes) } diff --git a/p2p/conn/connection.go b/p2p/conn/connection.go index e0ce062ab..ee29fc85c 100644 --- a/p2p/conn/connection.go +++ b/p2p/conn/connection.go @@ -707,7 +707,7 @@ type Channel struct { func newChannel(conn *MConnection, desc ChannelDescriptor) *Channel { desc = desc.FillDefaults() if desc.Priority <= 0 { - cmn.PanicSanity("Channel default priority must be a positive integer") + panic("Channel default priority must be a positive integer") } return &Channel{ conn: conn, diff --git a/p2p/netaddress.go b/p2p/netaddress.go index 5534ded98..73130ee5e 100644 --- a/p2p/netaddress.go +++ b/p2p/netaddress.go @@ -14,8 +14,6 @@ import ( "time" "errors" - - cmn "github.com/tendermint/tendermint/libs/common" ) // NetAddress defines information about a peer on the network @@ -48,7 +46,7 @@ func NewNetAddress(id ID, addr net.Addr) *NetAddress { tcpAddr, ok := addr.(*net.TCPAddr) if !ok { if flag.Lookup("test.v") == nil { // normal run - cmn.PanicSanity(fmt.Sprintf("Only TCPAddrs are supported. Got: %v", addr)) + panic(fmt.Sprintf("Only TCPAddrs are supported. Got: %v", addr)) } else { // in testing netAddr := NewNetAddressIPPort(net.IP("0.0.0.0"), 0) netAddr.ID = id diff --git a/p2p/pex/file.go b/p2p/pex/file.go index d4a516850..a42eddaf9 100644 --- a/p2p/pex/file.go +++ b/p2p/pex/file.go @@ -53,14 +53,14 @@ func (a *addrBook) loadFromFile(filePath string) bool { // Load addrBookJSON{} r, err := os.Open(filePath) if err != nil { - cmn.PanicCrisis(fmt.Sprintf("Error opening file %s: %v", filePath, err)) + panic(fmt.Sprintf("Error opening file %s: %v", filePath, err)) } defer r.Close() // nolint: errcheck aJSON := &addrBookJSON{} dec := json.NewDecoder(r) err = dec.Decode(aJSON) if err != nil { - cmn.PanicCrisis(fmt.Sprintf("Error reading file %s: %v", filePath, err)) + panic(fmt.Sprintf("Error reading file %s: %v", filePath, err)) } // Restore all the fields... diff --git a/p2p/switch.go b/p2p/switch.go index afd7d9656..a566e6029 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -155,7 +155,7 @@ func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { for _, chDesc := range reactorChannels { chID := chDesc.ID if sw.reactorsByCh[chID] != nil { - cmn.PanicSanity(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor)) + panic(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor)) } sw.chDescs = append(sw.chDescs, chDesc) sw.reactorsByCh[chID] = reactor diff --git a/p2p/trust/store.go b/p2p/trust/store.go index d6b4c049d..fc1ad399e 100644 --- a/p2p/trust/store.go +++ b/p2p/trust/store.go @@ -156,7 +156,7 @@ func (tms *TrustMetricStore) loadFromDB() bool { peers := make(map[string]MetricHistoryJSON) err := json.Unmarshal(bytes, &peers) if err != nil { - cmn.PanicCrisis(fmt.Sprintf("Could not unmarshal Trust Metric Store DB data: %v", err)) + panic(fmt.Sprintf("Could not unmarshal Trust Metric Store DB data: %v", err)) } // If history data exists in the file, diff --git a/types/part_set.go b/types/part_set.go index 4533fb759..389db7a0b 100644 --- a/types/part_set.go +++ b/types/part_set.go @@ -226,7 +226,7 @@ func (ps *PartSet) IsComplete() bool { func (ps *PartSet) GetReader() io.Reader { if !ps.IsComplete() { - cmn.PanicSanity("Cannot GetReader() on incomplete PartSet") + panic("Cannot GetReader() on incomplete PartSet") } return NewPartSetReader(ps.parts) } diff --git a/types/validator.go b/types/validator.go index 325d20f5c..a662eb6c0 100644 --- a/types/validator.go +++ b/types/validator.go @@ -52,8 +52,7 @@ func (v *Validator) CompareProposerPriority(other *Validator) *Validator { } else if result > 0 { return other } else { - cmn.PanicSanity("Cannot compare identical validators") - return nil + panic("Cannot compare identical validators") } } } diff --git a/types/vote.go b/types/vote.go index ad05d688d..6fcbd3ff6 100644 --- a/types/vote.go +++ b/types/vote.go @@ -93,7 +93,7 @@ func (vote *Vote) String() string { case PrecommitType: typeString = "Precommit" default: - cmn.PanicSanity("Unknown vote type") + panic("Unknown vote type") } return fmt.Sprintf("Vote{%v:%X %v/%02d/%v(%v) %X %X @ %s}", diff --git a/types/vote_set.go b/types/vote_set.go index 1cd0f2281..2c896b39c 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -70,7 +70,7 @@ type VoteSet struct { // Constructs a new VoteSet struct used to accumulate votes for given height/round. func NewVoteSet(chainID string, height int64, round int, type_ SignedMsgType, valSet *ValidatorSet) *VoteSet { if height == 0 { - cmn.PanicSanity("Cannot make VoteSet for height == 0, doesn't make sense.") + panic("Cannot make VoteSet for height == 0, doesn't make sense.") } return &VoteSet{ chainID: chainID, @@ -130,7 +130,7 @@ func (voteSet *VoteSet) Size() int { // NOTE: Vote must not be nil func (voteSet *VoteSet) AddVote(vote *Vote) (added bool, err error) { if voteSet == nil { - cmn.PanicSanity("AddVote() on nil VoteSet") + panic("AddVote() on nil VoteSet") } voteSet.mtx.Lock() defer voteSet.mtx.Unlock() @@ -196,7 +196,7 @@ func (voteSet *VoteSet) addVote(vote *Vote) (added bool, err error) { return added, NewConflictingVoteError(val, conflicting, vote) } if !added { - cmn.PanicSanity("Expected to add non-conflicting vote") + panic("Expected to add non-conflicting vote") } return added, nil } @@ -220,7 +220,7 @@ func (voteSet *VoteSet) addVerifiedVote(vote *Vote, blockKey string, votingPower // Already exists in voteSet.votes? if existing := voteSet.votes[valIndex]; existing != nil { if existing.BlockID.Equals(vote.BlockID) { - cmn.PanicSanity("addVerifiedVote does not expect duplicate votes") + panic("addVerifiedVote does not expect duplicate votes") } else { conflicting = existing } @@ -290,7 +290,7 @@ func (voteSet *VoteSet) addVerifiedVote(vote *Vote, blockKey string, votingPower // NOTE: VoteSet must not be nil func (voteSet *VoteSet) SetPeerMaj23(peerID P2PID, blockID BlockID) error { if voteSet == nil { - cmn.PanicSanity("SetPeerMaj23() on nil VoteSet") + panic("SetPeerMaj23() on nil VoteSet") } voteSet.mtx.Lock() defer voteSet.mtx.Unlock() @@ -363,7 +363,7 @@ func (voteSet *VoteSet) GetByAddress(address []byte) *Vote { defer voteSet.mtx.Unlock() valIndex, val := voteSet.valSet.GetByAddress(address) if val == nil { - cmn.PanicSanity("GetByAddress(address) returned nil") + panic("GetByAddress(address) returned nil") } return voteSet.votes[valIndex] } @@ -530,14 +530,14 @@ func (voteSet *VoteSet) sumTotalFrac() (int64, int64, float64) { func (voteSet *VoteSet) MakeCommit() *Commit { if voteSet.type_ != PrecommitType { - cmn.PanicSanity("Cannot MakeCommit() unless VoteSet.Type is PrecommitType") + panic("Cannot MakeCommit() unless VoteSet.Type is PrecommitType") } voteSet.mtx.Lock() defer voteSet.mtx.Unlock() // Make sure we have a 2/3 majority if voteSet.maj23 == nil { - cmn.PanicSanity("Cannot MakeCommit() unless a blockhash has +2/3") + panic("Cannot MakeCommit() unless a blockhash has +2/3") } // For every validator, get the precommit From 7b162f5c54267d716ef2e66912d59a0bd993a84f Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 26 Apr 2019 08:05:39 -0400 Subject: [PATCH 012/211] node: refactor node.NewNode (#3456) The node.NewNode method is pretty complex at the moment, an in order to address issues like #3156, we need to simplify the interface for partial node instantiation. In some places, we don't need to build up a full node (like in the node.TestCreateProposalBlock test), but the complexity of such partial instantiation needs to be reduced. This PR aims to eventually make this easier/simpler. See also this gist https://gist.github.com/thanethomson/56e1640d057a26186e38ad678a1d114c for some background work done when starting to refactor here. ## Commits: * [WIP] Refactor node.NewNode to simplify The `node.NewNode` method is pretty complex at the moment, an in order to address issues like #3156, we need to simplify the interface for partial node instantiation. In some places, we don't need to build up a full node (like in the `node.TestCreateProposalBlock` test), but the complexity of such partial instantiation needs to be reduced. This PR aims to eventually make this easier/simpler. * Refactor state loading and genesis doc provider into state package * Refactor for clarity of return parameters * Fix incorrect capitalization of error messages * Simplify extracted functions' names * Document optionally-prefixed functions * Refactor optionallyFastSync for clarity of separation of concerns * Restructure function for early return * Restructure function for early return * Remove dependence on deprecated panic functions * refactor code a bit more plus, expose PEXReactor on node * align logger names * add a changelog entry * align logger names 2 * add a note about PEXReactor returning nil --- CHANGELOG_PENDING.md | 3 +- node/node.go | 458 +++++++++++++++++++++++++------------------ rpc/test/helpers.go | 3 +- state/store.go | 65 ++++++ 4 files changed, 331 insertions(+), 198 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 6065ee9b8..1dc44e686 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -9,7 +9,8 @@ * Apps * Go API -- [libs/common] Removed PanicSanity, PanicCrisis, PanicConsensus and PanicQ +- [libs/common] Removed `PanicSanity`, `PanicCrisis`, `PanicConsensus` and `PanicQ` +- [node] Moved `GenesisDocProvider` and `DefaultGenesisDocProviderFunc` to state package * Blockchain Protocol diff --git a/node/node.go b/node/node.go index 9ad7172fb..36bdb9d57 100644 --- a/node/node.go +++ b/node/node.go @@ -18,8 +18,10 @@ import ( amino "github.com/tendermint/go-amino" abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/blockchain" bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/consensus" cs "github.com/tendermint/tendermint/consensus" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/evidence" @@ -63,19 +65,6 @@ func DefaultDBProvider(ctx *DBContext) (dbm.DB, error) { return dbm.NewDB(ctx.ID, dbType, ctx.Config.DBDir()), nil } -// GenesisDocProvider returns a GenesisDoc. -// It allows the GenesisDoc to be pulled from sources other than the -// filesystem, for instance from a distributed key-value store cluster. -type GenesisDocProvider func() (*types.GenesisDoc, error) - -// DefaultGenesisDocProviderFunc returns a GenesisDocProvider that loads -// the GenesisDoc from the config.GenesisFile() on the filesystem. -func DefaultGenesisDocProviderFunc(config *cfg.Config) GenesisDocProvider { - return func() (*types.GenesisDoc, error) { - return types.GenesisDocFromFile(config.GenesisFile()) - } -} - // NodeProvider takes a config and a logger and returns a ready to go Node. type NodeProvider func(*cfg.Config, log.Logger) (*Node, error) @@ -96,7 +85,7 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { if _, err := os.Stat(oldPrivVal); !os.IsNotExist(err) { oldPV, err := privval.LoadOldFilePV(oldPrivVal) if err != nil { - return nil, fmt.Errorf("Error reading OldPrivValidator from %v: %v\n", oldPrivVal, err) + return nil, fmt.Errorf("error reading OldPrivValidator from %v: %v\n", oldPrivVal, err) } logger.Info("Upgrading PrivValidator file", "old", oldPrivVal, @@ -110,7 +99,7 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { privval.LoadOrGenFilePV(newPrivValKey, newPrivValState), nodeKey, proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), - DefaultGenesisDocProviderFunc(config), + sm.DefaultGenesisDocProviderFunc(config), DefaultDBProvider, DefaultMetricsProvider(config.Instrumentation), logger, @@ -162,6 +151,7 @@ type Node struct { mempoolReactor *mempl.MempoolReactor // for gossipping transactions consensusState *cs.ConsensusState // latest consensus state consensusReactor *cs.ConsensusReactor // for participating in the consensus + pexReactor *pex.PEXReactor // for exchanging peer addresses evidencePool *evidence.EvidencePool // tracking evidence proxyApp proxy.AppConns // connection to the application rpcListeners []net.Listener // rpc servers @@ -170,73 +160,49 @@ type Node struct { prometheusSrv *http.Server } -// NewNode returns a new, ready to go, Tendermint Node. -func NewNode(config *cfg.Config, - privValidator types.PrivValidator, - nodeKey *p2p.NodeKey, - clientCreator proxy.ClientCreator, - genesisDocProvider GenesisDocProvider, - dbProvider DBProvider, - metricsProvider MetricsProvider, - logger log.Logger) (*Node, error) { - - // Get BlockStore - blockStoreDB, err := dbProvider(&DBContext{"blockstore", config}) - if err != nil { - return nil, err - } - blockStore := bc.NewBlockStore(blockStoreDB) - - // Get State - stateDB, err := dbProvider(&DBContext{"state", config}) +func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *bc.BlockStore, stateDB dbm.DB, err error) { + var blockStoreDB dbm.DB + blockStoreDB, err = dbProvider(&DBContext{"blockstore", config}) if err != nil { - return nil, err + return } + blockStore = bc.NewBlockStore(blockStoreDB) - // Get genesis doc - // TODO: move to state package? - genDoc, err := loadGenesisDoc(stateDB) + stateDB, err = dbProvider(&DBContext{"state", config}) if err != nil { - genDoc, err = genesisDocProvider() - if err != nil { - return nil, err - } - // save genesis doc to prevent a certain class of user errors (e.g. when it - // was changed, accidentally or not). Also good for audit trail. - saveGenesisDoc(stateDB, genDoc) + return } - state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) - if err != nil { - return nil, err - } + return +} - // Create the proxyApp and establish connections to the ABCI app (consensus, mempool, query). +func createAndStartProxyAppConns(clientCreator proxy.ClientCreator, logger log.Logger) (proxy.AppConns, error) { proxyApp := proxy.NewAppConns(clientCreator) proxyApp.SetLogger(logger.With("module", "proxy")) if err := proxyApp.Start(); err != nil { - return nil, fmt.Errorf("Error starting proxy app connections: %v", err) + return nil, fmt.Errorf("error starting proxy app connections: %v", err) } + return proxyApp, nil +} - // EventBus and IndexerService must be started before the handshake because - // we might need to index the txs of the replayed block as this might not have happened - // when the node stopped last time (i.e. the node stopped after it saved the block - // but before it indexed the txs, or, endblocker panicked) +func createAndStartEventBus(logger log.Logger) (*types.EventBus, error) { eventBus := types.NewEventBus() eventBus.SetLogger(logger.With("module", "events")) - - err = eventBus.Start() - if err != nil { + if err := eventBus.Start(); err != nil { return nil, err } + return eventBus, nil +} + +func createAndStartIndexerService(config *cfg.Config, dbProvider DBProvider, + eventBus *types.EventBus, logger log.Logger) (*txindex.IndexerService, txindex.TxIndexer, error) { - // Transaction indexing var txIndexer txindex.TxIndexer switch config.TxIndex.Indexer { case "kv": store, err := dbProvider(&DBContext{"tx_index", config}) if err != nil { - return nil, err + return nil, nil, err } if config.TxIndex.IndexTags != "" { txIndexer = kv.NewTxIndex(store, kv.IndexTags(splitAndTrimEmpty(config.TxIndex.IndexTags, ",", " "))) @@ -251,26 +217,26 @@ func NewNode(config *cfg.Config, indexerService := txindex.NewIndexerService(txIndexer, eventBus) indexerService.SetLogger(logger.With("module", "txindex")) - - err = indexerService.Start() - if err != nil { - return nil, err + if err := indexerService.Start(); err != nil { + return nil, nil, err } + return indexerService, txIndexer, nil +} + +func doHandshake(stateDB dbm.DB, state sm.State, blockStore sm.BlockStore, + genDoc *types.GenesisDoc, eventBus *types.EventBus, proxyApp proxy.AppConns, consensusLogger log.Logger) error { - // Create the handshaker, which calls RequestInfo, sets the AppVersion on the state, - // and replays any blocks as necessary to sync tendermint with the app. - consensusLogger := logger.With("module", "consensus") handshaker := cs.NewHandshaker(stateDB, state, blockStore, genDoc) handshaker.SetLogger(consensusLogger) handshaker.SetEventBus(eventBus) if err := handshaker.Handshake(proxyApp); err != nil { - return nil, fmt.Errorf("Error during handshake: %v", err) + return fmt.Errorf("error during handshake: %v", err) } + return nil +} - // Reload the state. It will have the Version.Consensus.App set by the - // Handshake, and may have other modifications as well (ie. depending on - // what happened during block replay). - state = sm.LoadState(stateDB) +func logNodeStartupInfo(state sm.State, privValidator types.PrivValidator, logger, + consensusLogger log.Logger) { // Log the version info. logger.Info("Version info", @@ -287,27 +253,6 @@ func NewNode(config *cfg.Config, ) } - if config.PrivValidatorListenAddr != "" { - // If an address is provided, listen on the socket for a connection from an - // external signing process. - // FIXME: we should start services inside OnStart - privValidator, err = createAndStartPrivValidatorSocketClient(config.PrivValidatorListenAddr, logger) - if err != nil { - return nil, errors.Wrap(err, "Error with private validator socket client") - } - } - - // Decide whether to fast-sync or not - // We don't fast-sync when the only validator is us. - fastSync := config.FastSync - if state.Validators.Size() == 1 { - addr, _ := state.Validators.GetByIndex(0) - privValAddr := privValidator.GetPubKey().Address() - if bytes.Equal(privValAddr, addr) { - fastSync = false - } - } - pubKey := privValidator.GetPubKey() addr := pubKey.Address() // Log whether this node is a validator or an observer @@ -316,10 +261,19 @@ func NewNode(config *cfg.Config, } else { consensusLogger.Info("This node is not a validator", "addr", addr, "pubKey", pubKey) } +} - csMetrics, p2pMetrics, memplMetrics, smMetrics := metricsProvider(genDoc.ChainID) +func onlyValidatorIsUs(state sm.State, privVal types.PrivValidator) bool { + if state.Validators.Size() > 1 { + return false + } + addr, _ := state.Validators.GetByIndex(0) + return bytes.Equal(privVal.GetPubKey().Address(), addr) +} + +func createMempoolAndMempoolReactor(config *cfg.Config, proxyApp proxy.AppConns, + state sm.State, memplMetrics *mempl.Metrics, logger log.Logger) (*mempl.MempoolReactor, *mempl.Mempool) { - // Make MempoolReactor mempool := mempl.NewMempool( config.Mempool, proxyApp.Mempool(), @@ -339,34 +293,36 @@ func NewNode(config *cfg.Config, if config.Consensus.WaitForTxs() { mempool.EnableTxsAvailable() } + return mempoolReactor, mempool +} + +func createEvidenceReactor(config *cfg.Config, dbProvider DBProvider, + stateDB dbm.DB, logger log.Logger) (*evidence.EvidenceReactor, *evidence.EvidencePool, error) { - // Make Evidence Reactor evidenceDB, err := dbProvider(&DBContext{"evidence", config}) if err != nil { - return nil, err + return nil, nil, err } evidenceLogger := logger.With("module", "evidence") evidencePool := evidence.NewEvidencePool(stateDB, evidenceDB) evidencePool.SetLogger(evidenceLogger) evidenceReactor := evidence.NewEvidenceReactor(evidencePool) evidenceReactor.SetLogger(evidenceLogger) + return evidenceReactor, evidencePool, nil +} - blockExecLogger := logger.With("module", "state") - // make block executor for consensus and blockchain reactors to execute blocks - blockExec := sm.NewBlockExecutor( - stateDB, - blockExecLogger, - proxyApp.Consensus(), - mempool, - evidencePool, - sm.BlockExecutorWithMetrics(smMetrics), - ) - - // Make BlockchainReactor - bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) - bcReactor.SetLogger(logger.With("module", "blockchain")) +func createConsensusReactor(config *cfg.Config, + state sm.State, + blockExec *sm.BlockExecutor, + blockStore sm.BlockStore, + mempool *mempl.Mempool, + evidencePool *evidence.EvidencePool, + privValidator types.PrivValidator, + csMetrics *cs.Metrics, + fastSync bool, + eventBus *types.EventBus, + consensusLogger log.Logger) (*consensus.ConsensusReactor, *consensus.ConsensusState) { - // Make ConsensusReactor consensusState := cs.NewConsensusState( config.Consensus, state.Copy(), @@ -382,28 +338,13 @@ func NewNode(config *cfg.Config, } consensusReactor := cs.NewConsensusReactor(consensusState, fastSync, cs.ReactorMetrics(csMetrics)) consensusReactor.SetLogger(consensusLogger) - // services which will be publishing and/or subscribing for messages (events) // consensusReactor will set it on consensusState and blockExecutor consensusReactor.SetEventBus(eventBus) + return consensusReactor, consensusState +} - p2pLogger := logger.With("module", "p2p") - nodeInfo, err := makeNodeInfo( - config, - nodeKey.ID(), - txIndexer, - genDoc.ChainID, - p2p.NewProtocolVersion( - version.P2PProtocol, // global - state.Version.Consensus.Block, - state.Version.Consensus.App, - ), - ) - if err != nil { - return nil, err - } - - // Setup Transport. +func createTransport(config *cfg.Config, nodeInfo p2p.NodeInfo, nodeKey *p2p.NodeKey, proxyApp proxy.AppConns) (*p2p.MultiplexTransport, []p2p.PeerFilterFunc) { var ( mConnConfig = p2p.MConnConfig(config.P2P) transport = p2p.NewMultiplexTransport(nodeInfo, *nodeKey, mConnConfig) @@ -429,7 +370,7 @@ func NewNode(config *cfg.Config, return err } if res.IsErr() { - return fmt.Errorf("Error querying abci app: %v", res) + return fmt.Errorf("error querying abci app: %v", res) } return nil @@ -447,7 +388,7 @@ func NewNode(config *cfg.Config, return err } if res.IsErr() { - return fmt.Errorf("Error querying abci app: %v", res) + return fmt.Errorf("error querying abci app: %v", res) } return nil @@ -456,8 +397,21 @@ func NewNode(config *cfg.Config, } p2p.MultiplexTransportConnFilters(connFilters...)(transport) + return transport, peerFilters +} + +func createSwitch(config *cfg.Config, + transport *p2p.MultiplexTransport, + p2pMetrics *p2p.Metrics, + peerFilters []p2p.PeerFilterFunc, + mempoolReactor *mempl.MempoolReactor, + bcReactor *blockchain.BlockchainReactor, + consensusReactor *consensus.ConsensusReactor, + evidenceReactor *evidence.EvidenceReactor, + nodeInfo p2p.NodeInfo, + nodeKey *p2p.NodeKey, + p2pLogger log.Logger) *p2p.Switch { - // Setup Switch. sw := p2p.NewSwitch( config.P2P, transport, @@ -473,6 +427,159 @@ func NewNode(config *cfg.Config, sw.SetNodeKey(nodeKey) p2pLogger.Info("P2P Node ID", "ID", nodeKey.ID(), "file", config.NodeKeyFile()) + return sw +} + +func createAddrBookAndSetOnSwitch(config *cfg.Config, sw *p2p.Switch, + p2pLogger log.Logger) pex.AddrBook { + + addrBook := pex.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) + addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) + + // Add ourselves to addrbook to prevent dialing ourselves + addrBook.AddOurAddress(sw.NetAddress()) + + sw.SetAddrBook(addrBook) + + return addrBook +} + +func createPEXReactorAndAddToSwitch(addrBook pex.AddrBook, config *cfg.Config, + sw *p2p.Switch, logger log.Logger) *pex.PEXReactor { + + // TODO persistent peers ? so we can have their DNS addrs saved + pexReactor := pex.NewPEXReactor(addrBook, + &pex.PEXReactorConfig{ + Seeds: splitAndTrimEmpty(config.P2P.Seeds, ",", " "), + SeedMode: config.P2P.SeedMode, + // See consensus/reactor.go: blocksToContributeToBecomeGoodPeer 10000 + // blocks assuming 10s blocks ~ 28 hours. + // TODO (melekes): make it dynamic based on the actual block latencies + // from the live network. + // https://github.com/tendermint/tendermint/issues/3523 + SeedDisconnectWaitPeriod: 28 * time.Hour, + }) + pexReactor.SetLogger(logger.With("module", "pex")) + sw.AddReactor("PEX", pexReactor) + return pexReactor +} + +// NewNode returns a new, ready to go, Tendermint Node. +func NewNode(config *cfg.Config, + privValidator types.PrivValidator, + nodeKey *p2p.NodeKey, + clientCreator proxy.ClientCreator, + genesisDocProvider sm.GenesisDocProvider, + dbProvider DBProvider, + metricsProvider MetricsProvider, + logger log.Logger) (*Node, error) { + + blockStore, stateDB, err := initDBs(config, dbProvider) + if err != nil { + return nil, err + } + + state, genDoc, err := sm.LoadStateFromDBOrGenesisDocProvider(stateDB, genesisDocProvider) + if err != nil { + return nil, err + } + + // Create the proxyApp and establish connections to the ABCI app (consensus, mempool, query). + proxyApp, err := createAndStartProxyAppConns(clientCreator, logger) + if err != nil { + return nil, err + } + + // EventBus and IndexerService must be started before the handshake because + // we might need to index the txs of the replayed block as this might not have happened + // when the node stopped last time (i.e. the node stopped after it saved the block + // but before it indexed the txs, or, endblocker panicked) + eventBus, err := createAndStartEventBus(logger) + if err != nil { + return nil, err + } + + // Transaction indexing + indexerService, txIndexer, err := createAndStartIndexerService(config, dbProvider, eventBus, logger) + if err != nil { + return nil, err + } + + // Create the handshaker, which calls RequestInfo, sets the AppVersion on the state, + // and replays any blocks as necessary to sync tendermint with the app. + consensusLogger := logger.With("module", "consensus") + if err := doHandshake(stateDB, state, blockStore, genDoc, eventBus, proxyApp, consensusLogger); err != nil { + return nil, err + } + + // Reload the state. It will have the Version.Consensus.App set by the + // Handshake, and may have other modifications as well (ie. depending on + // what happened during block replay). + state = sm.LoadState(stateDB) + + // If an address is provided, listen on the socket for a connection from an + // external signing process. + if config.PrivValidatorListenAddr != "" { + // FIXME: we should start services inside OnStart + privValidator, err = createAndStartPrivValidatorSocketClient(config.PrivValidatorListenAddr, logger) + if err != nil { + return nil, errors.Wrap(err, "error with private validator socket client") + } + } + + logNodeStartupInfo(state, privValidator, logger, consensusLogger) + + // Decide whether to fast-sync or not + // We don't fast-sync when the only validator is us. + fastSync := config.FastSync && !onlyValidatorIsUs(state, privValidator) + + csMetrics, p2pMetrics, memplMetrics, smMetrics := metricsProvider(genDoc.ChainID) + + // Make MempoolReactor + mempoolReactor, mempool := createMempoolAndMempoolReactor(config, proxyApp, state, memplMetrics, logger) + + // Make Evidence Reactor + evidenceReactor, evidencePool, err := createEvidenceReactor(config, dbProvider, stateDB, logger) + if err != nil { + return nil, err + } + + // make block executor for consensus and blockchain reactors to execute blocks + blockExec := sm.NewBlockExecutor( + stateDB, + logger.With("module", "state"), + proxyApp.Consensus(), + mempool, + evidencePool, + sm.BlockExecutorWithMetrics(smMetrics), + ) + + // Make BlockchainReactor + bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor.SetLogger(logger.With("module", "blockchain")) + + // Make ConsensusReactor + consensusReactor, consensusState := createConsensusReactor( + config, state, blockExec, blockStore, mempool, evidencePool, + privValidator, csMetrics, fastSync, eventBus, consensusLogger, + ) + + nodeInfo, err := makeNodeInfo(config, nodeKey, txIndexer, genDoc, state) + if err != nil { + return nil, err + } + + // Setup Transport. + transport, peerFilters := createTransport(config, nodeInfo, nodeKey, proxyApp) + + // Setup Switch. + p2pLogger := logger.With("module", "p2p") + sw := createSwitch( + config, transport, p2pMetrics, peerFilters, mempoolReactor, bcReactor, + consensusReactor, evidenceReactor, nodeInfo, nodeKey, p2pLogger, + ) + + addrBook := createAddrBookAndSetOnSwitch(config, sw, p2pLogger) // Optionally, start the pex reactor // @@ -486,37 +593,13 @@ func NewNode(config *cfg.Config, // // If PEX is on, it should handle dialing the seeds. Otherwise the switch does it. // Note we currently use the addrBook regardless at least for AddOurAddress - addrBook := pex.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) - - // Add ourselves to addrbook to prevent dialing ourselves - addrBook.AddOurAddress(sw.NetAddress()) - - addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) + var pexReactor *pex.PEXReactor if config.P2P.PexReactor { - // TODO persistent peers ? so we can have their DNS addrs saved - pexReactor := pex.NewPEXReactor(addrBook, - &pex.PEXReactorConfig{ - Seeds: splitAndTrimEmpty(config.P2P.Seeds, ",", " "), - SeedMode: config.P2P.SeedMode, - // See consensus/reactor.go: blocksToContributeToBecomeGoodPeer 10000 - // blocks assuming 10s blocks ~ 28 hours. - // TODO (melekes): make it dynamic based on the actual block latencies - // from the live network. - // https://github.com/tendermint/tendermint/issues/3523 - SeedDisconnectWaitPeriod: 28 * time.Hour, - }) - pexReactor.SetLogger(logger.With("module", "pex")) - sw.AddReactor("PEX", pexReactor) + pexReactor = createPEXReactorAndAddToSwitch(addrBook, config, sw, logger) } - sw.SetAddrBook(addrBook) - - // run the profile server - profileHost := config.ProfListenAddress - if profileHost != "" { - go func() { - logger.Error("Profile server", "err", http.ListenAndServe(profileHost, nil)) - }() + if config.ProfListenAddress != "" { + go logger.Error("Profile server", "err", http.ListenAndServe(config.ProfListenAddress, nil)) } node := &Node{ @@ -536,6 +619,7 @@ func NewNode(config *cfg.Config, mempoolReactor: mempoolReactor, consensusState: consensusState, consensusReactor: consensusReactor, + pexReactor: pexReactor, evidencePool: evidencePool, proxyApp: proxyApp, txIndexer: txIndexer, @@ -804,6 +888,11 @@ func (n *Node) MempoolReactor() *mempl.MempoolReactor { return n.mempoolReactor } +// PEXReactor returns the Node's PEXReactor. It returns nil if PEX is disabled. +func (n *Node) PEXReactor() *pex.PEXReactor { + return n.pexReactor +} + // EvidencePool returns the Node's EvidencePool. func (n *Node) EvidencePool() *evidence.EvidencePool { return n.evidencePool @@ -854,20 +943,24 @@ func (n *Node) NodeInfo() p2p.NodeInfo { func makeNodeInfo( config *cfg.Config, - nodeID p2p.ID, + nodeKey *p2p.NodeKey, txIndexer txindex.TxIndexer, - chainID string, - protocolVersion p2p.ProtocolVersion, + genDoc *types.GenesisDoc, + state sm.State, ) (p2p.NodeInfo, error) { txIndexerStatus := "on" if _, ok := txIndexer.(*null.TxIndex); ok { txIndexerStatus = "off" } nodeInfo := p2p.DefaultNodeInfo{ - ProtocolVersion: protocolVersion, - ID_: nodeID, - Network: chainID, - Version: version.TMCoreSemVer, + ProtocolVersion: p2p.NewProtocolVersion( + version.P2PProtocol, // global + state.Version.Consensus.Block, + state.Version.Consensus.App, + ), + ID_: nodeKey.ID(), + Network: genDoc.ChainID, + Version: version.TMCoreSemVer, Channels: []byte{ bc.BlockchainChannel, cs.StateChannel, cs.DataChannel, cs.VoteChannel, cs.VoteSetBitsChannel, @@ -899,33 +992,6 @@ func makeNodeInfo( //------------------------------------------------------------------------------ -var ( - genesisDocKey = []byte("genesisDoc") -) - -// panics if failed to unmarshal bytes -func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) { - bytes := db.Get(genesisDocKey) - if len(bytes) == 0 { - return nil, errors.New("Genesis doc not found") - } - var genDoc *types.GenesisDoc - err := cdc.UnmarshalJSON(bytes, &genDoc) - if err != nil { - panic(fmt.Sprintf("Failed to load genesis doc due to unmarshaling error: %v (bytes: %X)", err, bytes)) - } - return genDoc, nil -} - -// panics if failed to marshal the given genesis document -func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) { - bytes, err := cdc.MarshalJSON(genDoc) - if err != nil { - panic(fmt.Sprintf("Failed to save genesis doc due to marshaling error: %v", err)) - } - db.SetSync(genesisDocKey, bytes) -} - func createAndStartPrivValidatorSocketClient( listenAddr string, logger log.Logger, @@ -946,7 +1012,7 @@ func createAndStartPrivValidatorSocketClient( listener = privval.NewTCPListener(ln, ed25519.GenPrivKey()) default: return nil, fmt.Errorf( - "Wrong listen address: expected either 'tcp' or 'unix' protocols, got %s", + "wrong listen address: expected either 'tcp' or 'unix' protocols, got %s", protocol, ) } diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index cddc80b8e..f5c923bf4 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -9,6 +9,7 @@ import ( "time" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/state" abci "github.com/tendermint/tendermint/abci/types" @@ -166,7 +167,7 @@ func NewTendermint(app abci.Application, opts *Options) *nm.Node { panic(err) } node, err := nm.NewNode(config, pv, nodeKey, papp, - nm.DefaultGenesisDocProviderFunc(config), + state.DefaultGenesisDocProviderFunc(config), nm.DefaultDBProvider, nm.DefaultMetricsProvider(config.Instrumentation), logger) diff --git a/state/store.go b/state/store.go index 0301bc7c3..e1fb5ba60 100644 --- a/state/store.go +++ b/state/store.go @@ -1,9 +1,11 @@ package state import ( + "errors" "fmt" abci "github.com/tendermint/tendermint/abci/types" + cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/types" @@ -17,6 +19,23 @@ const ( valSetCheckpointInterval = 100000 ) +var ( + genesisDocKey = []byte("genesisDoc") +) + +// GenesisDocProvider returns a GenesisDoc. +// It allows the GenesisDoc to be pulled from sources other than the +// filesystem, for instance from a distributed key-value store cluster. +type GenesisDocProvider func() (*types.GenesisDoc, error) + +// DefaultGenesisDocProviderFunc returns a GenesisDocProvider that loads +// the GenesisDoc from the config.GenesisFile() on the filesystem. +func DefaultGenesisDocProviderFunc(config *cfg.Config) GenesisDocProvider { + return func() (*types.GenesisDoc, error) { + return types.GenesisDocFromFile(config.GenesisFile()) + } +} + //------------------------------------------------------------------------ func calcValidatorsKey(height int64) []byte { @@ -65,6 +84,52 @@ func LoadStateFromDBOrGenesisDoc(stateDB dbm.DB, genesisDoc *types.GenesisDoc) ( return state, nil } +// LoadStateFromDBOrGenesisDocProvider attempts to load the state from the +// database, or creates one using the given genesisDocProvider and persists the +// result to the database. On success this also returns the genesis doc loaded +// through the given provider. +func LoadStateFromDBOrGenesisDocProvider(stateDB dbm.DB, genesisDocProvider GenesisDocProvider) (State, *types.GenesisDoc, error) { + // Get genesis doc + genDoc, err := loadGenesisDoc(stateDB) + if err != nil { + genDoc, err = genesisDocProvider() + if err != nil { + return State{}, nil, err + } + // save genesis doc to prevent a certain class of user errors (e.g. when it + // was changed, accidentally or not). Also good for audit trail. + saveGenesisDoc(stateDB, genDoc) + } + state, err := LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + if err != nil { + return State{}, nil, err + } + return state, genDoc, nil +} + +// panics if failed to unmarshal bytes +func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) { + bytes := db.Get(genesisDocKey) + if len(bytes) == 0 { + return nil, errors.New("Genesis doc not found") + } + var genDoc *types.GenesisDoc + err := cdc.UnmarshalJSON(bytes, &genDoc) + if err != nil { + panic(fmt.Sprintf("Failed to load genesis doc due to unmarshaling error: %v (bytes: %X)", err, bytes)) + } + return genDoc, nil +} + +// panics if failed to marshal the given genesis document +func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) { + bytes, err := cdc.MarshalJSON(genDoc) + if err != nil { + panic(fmt.Sprintf("Failed to save genesis doc due to marshaling error: %v", err)) + } + db.SetSync(genesisDocKey, bytes) +} + // LoadState loads the State from the database. func LoadState(db dbm.DB) State { return loadState(db, stateKey) From 40dbad9915561434dad539eb681834e8fd126d65 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Mon, 29 Apr 2019 23:17:57 -0700 Subject: [PATCH 013/211] pex: dial seeds when address book needs more addresses (#3603) If we are low on addresses for peering, we need to discover more peers. The previous behavior would query existing peers; however, if an existing peer does not participate in peer exchange, then our node will not discover more peers. This change consults both existing peers as well as seeds when there is a deficit in address book addresses. This allows for discovering peers though existing channels as well as via seeds if existing peers do not share addresses. --- CHANGELOG_PENDING.md | 1 + p2p/pex/pex_reactor.go | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index ff2ff69e3..c4441776a 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -27,3 +27,4 @@ - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode to 16 (as a result, the node will stop retrying after a 35 hours time window) - [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests +- [pex] \#3603 Dial seeds when addrbook needs more addresses (@defunctzombie) \ No newline at end of file diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index 38befe29a..b63c5f815 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -493,9 +493,10 @@ func (r *PEXReactor) ensurePeers() { } } - // If we are not connected to nor dialing anybody, fallback to dialing a seed. - if out+in+dial+len(toDial) == 0 { - r.Logger.Info("No addresses to dial nor connected peers. Falling back to seeds") + // If we are not dialing anyone and need more addresses - dial a seed + // This is done in addition to asking a peer for addresses to work-around peers not participating in PEX + if r.book.NeedMoreAddrs() && len(toDial) == 0 { + r.Logger.Info("No addresses to dial. Falling back to seeds") r.dialSeeds() } } From 43348022d6904a0d19d97fc1e711facecdc2b9d5 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 1 May 2019 09:38:26 +0400 Subject: [PATCH 014/211] pex: various follow-ups (#3605) * p2p: merge switch cases also improve the error msg in privval * pex: refactor code plus update specification follow-up to https://github.com/tendermint/tendermint/pull/3603 * Update docs/spec/reactors/pex/pex.md Co-Authored-By: melekes --- docs/spec/reactors/pex/pex.md | 19 +++++++++++-------- p2p/pex/pex_reactor.go | 23 ++++++++++------------- privval/socket_listeners_test.go | 4 ++-- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/spec/reactors/pex/pex.md b/docs/spec/reactors/pex/pex.md index 26f1fa8bb..268b4a318 100644 --- a/docs/spec/reactors/pex/pex.md +++ b/docs/spec/reactors/pex/pex.md @@ -21,17 +21,20 @@ inbound (they dialed our public address) or outbound (we dialed them). ## Discovery Peer discovery begins with a list of seeds. -When we have no peers, or have been unable to find enough peers from existing ones, -we dial a randomly selected seed to get a list of peers to dial. + +When we don't have enough peers, we + +1. ask existing peers +2. dial seeds if we're not dialing anyone currently On startup, we will also immediately dial the given list of `persistent_peers`, -and will attempt to maintain persistent connections with them. If the connections die, or we fail to dial, -we will redial every 5s for a few minutes, then switch to an exponential backoff schedule, -and after about a day of trying, stop dialing the peer. +and will attempt to maintain persistent connections with them. If the +connections die, or we fail to dial, we will redial every 5s for a few minutes, +then switch to an exponential backoff schedule, and after about a day of +trying, stop dialing the peer. -So long as we have less than `MaxNumOutboundPeers`, we periodically request additional peers -from each of our own. If sufficient time goes by and we still can't find enough peers, -we try the seeds again. +As long as we have less than `MaxNumOutboundPeers`, we periodically request +additional peers from each of our own and try seeds. ## Listening diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index b63c5f815..f24f44dd5 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -471,9 +471,7 @@ func (r *PEXReactor) ensurePeers() { err := r.dialPeer(addr) if err != nil { switch err.(type) { - case errMaxAttemptsToDial: - r.Logger.Debug(err.Error(), "addr", addr) - case errTooEarlyToDial: + case errMaxAttemptsToDial, errTooEarlyToDial: r.Logger.Debug(err.Error(), "addr", addr) default: r.Logger.Error(err.Error(), "addr", addr) @@ -482,8 +480,8 @@ func (r *PEXReactor) ensurePeers() { }(addr) } - // If we need more addresses, pick a random peer and ask for more. if r.book.NeedMoreAddrs() { + // 1) Pick a random peer and ask for more. peers := r.Switch.Peers().List() peersCount := len(peers) if peersCount > 0 { @@ -491,13 +489,14 @@ func (r *PEXReactor) ensurePeers() { r.Logger.Info("We need more addresses. Sending pexRequest to random peer", "peer", peer) r.RequestAddrs(peer) } - } - // If we are not dialing anyone and need more addresses - dial a seed - // This is done in addition to asking a peer for addresses to work-around peers not participating in PEX - if r.book.NeedMoreAddrs() && len(toDial) == 0 { - r.Logger.Info("No addresses to dial. Falling back to seeds") - r.dialSeeds() + // 2) Dial seeds if we are not dialing anyone. + // This is done in addition to asking a peer for addresses to work-around + // peers not participating in PEX. + if len(toDial) == 0 { + r.Logger.Info("No addresses to dial. Falling back to seeds") + r.dialSeeds() + } } } @@ -664,9 +663,7 @@ func (r *PEXReactor) crawlPeers(addrs []*p2p.NetAddress) { err := r.dialPeer(addr) if err != nil { switch err.(type) { - case errMaxAttemptsToDial: - r.Logger.Debug(err.Error(), "addr", addr) - case errTooEarlyToDial: + case errMaxAttemptsToDial, errTooEarlyToDial: r.Logger.Debug(err.Error(), "addr", addr) default: r.Logger.Error(err.Error(), "addr", addr) diff --git a/privval/socket_listeners_test.go b/privval/socket_listeners_test.go index 1cda2b6ec..3c4cb8588 100644 --- a/privval/socket_listeners_test.go +++ b/privval/socket_listeners_test.go @@ -130,8 +130,8 @@ func TestListenerTimeoutReadWrite(t *testing.T) { t.Errorf("for %s listener, have %v, want %v", tc.description, have, want) } - if have, want := opErr.Timeout(), true; have != want { - t.Errorf("for %s listener, got unexpected error: have %v, want %v", tc.description, have, want) + if !opErr.Timeout() { + t.Errorf("for %s listener, got unexpected error: have %v, want Timeout error", tc.description, opErr) } } } From a2a68df5219c45c57bd41216df18a9a1b56c0d84 Mon Sep 17 00:00:00 2001 From: Ivan Kushmantsev Date: Wed, 1 May 2019 10:01:41 +0400 Subject: [PATCH 015/211] cli: add option to not clear address book with unsafe reset (#3606) Fixes #3585 --- CHANGELOG_PENDING.md | 1 + cmd/tendermint/commands/reset_priv_validator.go | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index c4441776a..0f1ee28f0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,6 +20,7 @@ ### IMPROVEMENTS: - [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC +- [cli] \#3606 (https://github.com/tendermint/tendermint/issues/3585) Add option to not clear address book with unsafe reset (@climber73) - [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `-config=` option to `testnet` cmd (@gregdhill) - [cs/replay] \#3460 check appHash for each block diff --git a/cmd/tendermint/commands/reset_priv_validator.go b/cmd/tendermint/commands/reset_priv_validator.go index 055a76c51..4e8bde8b4 100644 --- a/cmd/tendermint/commands/reset_priv_validator.go +++ b/cmd/tendermint/commands/reset_priv_validator.go @@ -18,6 +18,12 @@ var ResetAllCmd = &cobra.Command{ Run: resetAll, } +var keepAddrBook bool + +func init() { + ResetAllCmd.Flags().BoolVar(&keepAddrBook, "keep-addr-book", false, "Keep the address book intact") +} + // ResetPrivValidatorCmd resets the private validator files. var ResetPrivValidatorCmd = &cobra.Command{ Use: "unsafe_reset_priv_validator", @@ -41,7 +47,11 @@ func resetPrivValidator(cmd *cobra.Command, args []string) { // ResetAll removes address book files plus all data, and resets the privValdiator data. // Exported so other CLI tools can use it. func ResetAll(dbDir, addrBookFile, privValKeyFile, privValStateFile string, logger log.Logger) { - removeAddrBook(addrBookFile, logger) + if keepAddrBook { + logger.Info("The address book remains intact") + } else { + removeAddrBook(addrBookFile, logger) + } if err := os.RemoveAll(dbDir); err == nil { logger.Info("Removed all blockchain history", "dir", dbDir) } else { From 2c26d95ab914730ce70445f8aefeb18ff90f8159 Mon Sep 17 00:00:00 2001 From: JamesRay <66258875@qq.com> Date: Thu, 2 May 2019 05:15:53 +0800 Subject: [PATCH 016/211] cs/replay: execCommitBlock should not read from state.lastValidators (#3067) * execCommitBlock should not read from state.lastValidators * fix height 1 * fix blockchain/reactor_test * fix consensus/mempool_test * fix consensus/reactor_test * fix consensus/replay_test * add CHANGELOG * fix consensus/reactor_test * fix consensus/replay_test * add a test for replay validators change * fix mem_pool test * fix byzantine test * remove a redundant code * reduce validator change blocks to 6 * fix * return peer0 config * seperate testName * seperate testName 1 * seperate testName 2 * seperate app db path * seperate app db path 1 * add a lock before startNet * move the lock to reactor_test * simulate just once * try to find problem * handshake only saveState when app version changed * update gometalinter to 3.0.0 (#3233) in the attempt to fix https://circleci.com/gh/tendermint/tendermint/43165 also code is simplified by running gofmt -s . remove unused vars enable linters we're currently passing remove deprecated linters (cherry picked from commit d47094550315c094512a242445e0dde24b5a03f5) * gofmt code * goimport code * change the bool name to testValidatorsChange * adjust receive kvstore.ProtocolVersion * adjust receive kvstore.ProtocolVersion 1 * adjust receive kvstore.ProtocolVersion 3 * fix merge execution.go * fix merge develop * fix merge develop 1 * fix run cleanupFunc * adjust code according to reviewers' opinion * modify the func name match the convention * simplify simulate a chain containing some validator change txs 1 * test CI error * Merge remote-tracking branch 'upstream/develop' into fixReplay 1 * fix pubsub_test * subscribeUnbuffered vote channel --- CHANGELOG_PENDING.md | 3 +- blockchain/reactor_test.go | 4 +- consensus/byzantine_test.go | 4 +- consensus/common_test.go | 47 ++++- consensus/mempool_test.go | 9 +- consensus/reactor_test.go | 7 +- consensus/replay.go | 12 +- consensus/replay_test.go | 380 +++++++++++++++++++++++++++++++----- consensus/state_test.go | 17 +- consensus/wal_generator.go | 3 +- libs/pubsub/pubsub_test.go | 7 +- state/execution.go | 45 +++-- state/execution_test.go | 4 +- 13 files changed, 444 insertions(+), 98 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 0f1ee28f0..f777c6e06 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -28,4 +28,5 @@ - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode to 16 (as a result, the node will stop retrying after a 35 hours time window) - [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests -- [pex] \#3603 Dial seeds when addrbook needs more addresses (@defunctzombie) \ No newline at end of file +- [consensus] \#3067 getBeginBlockValidatorInfo loads validators from stateDB instead of state +- [pex] \#3603 Dial seeds when addrbook needs more addresses (@defunctzombie) diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 1b3aee56f..9cd3489c8 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -91,8 +91,10 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals // NOTE we have to create and commit the blocks first because // pool.height is determined from the store. fastSync := true - blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), proxyApp.Consensus(), + db := dbm.NewMemDB() + blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), sm.MockMempool{}, sm.MockEvidencePool{}) + sm.SaveState(db, state) // let's add some blocks in for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index 0c1b88565..d181fb1a6 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/p2p" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -69,7 +70,7 @@ func TestByzantine(t *testing.T) { blocksSubs[i], err = eventBus.Subscribe(context.Background(), testSubscriber, types.EventQueryNewBlock) require.NoError(t, err) - conR := NewConsensusReactor(css[i], true) // so we dont start the consensus states + conR := NewConsensusReactor(css[i], true) // so we don't start the consensus states conR.SetLogger(logger.With("validator", i)) conR.SetEventBus(eventBus) @@ -81,6 +82,7 @@ func TestByzantine(t *testing.T) { } reactors[i] = conRI + sm.SaveState(css[i].blockExec.DB(), css[i].state) //for save height 1's validators info } defer func() { diff --git a/consensus/common_test.go b/consensus/common_test.go index 8f305139e..a0c179ae1 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -14,6 +14,8 @@ import ( "github.com/go-kit/kit/log/term" + "path" + abcicli "github.com/tendermint/tendermint/abci/client" "github.com/tendermint/tendermint/abci/example/counter" "github.com/tendermint/tendermint/abci/example/kvstore" @@ -119,6 +121,24 @@ func incrementRound(vss ...*validatorStub) { } } +type ValidatorStubsByAddress []*validatorStub + +func (vss ValidatorStubsByAddress) Len() int { + return len(vss) +} + +func (vss ValidatorStubsByAddress) Less(i, j int) bool { + return bytes.Compare(vss[i].GetPubKey().Address(), vss[j].GetPubKey().Address()) == -1 +} + +func (vss ValidatorStubsByAddress) Swap(i, j int) { + it := vss[i] + vss[i] = vss[j] + vss[i].Index = i + vss[j] = it + vss[j].Index = j +} + //------------------------------------------------------------------------------- // Functions for transitioning the consensus state @@ -228,7 +248,7 @@ func validatePrevoteAndPrecommit(t *testing.T, cs *ConsensusState, thisRound, lo } func subscribeToVoter(cs *ConsensusState, addr []byte) <-chan tmpubsub.Message { - votesSub, err := cs.eventBus.Subscribe(context.Background(), testSubscriber, types.EventQueryVote) + votesSub, err := cs.eventBus.SubscribeUnbuffered(context.Background(), testSubscriber, types.EventQueryVote) if err != nil { panic(fmt.Sprintf("failed to subscribe %s to %v", testSubscriber, types.EventQueryVote)) } @@ -278,7 +298,8 @@ func newConsensusStateWithConfigAndBlockStore(thisConfig *cfg.Config, state sm.S evpool := sm.MockEvidencePool{} // Make ConsensusState - stateDB := dbm.NewMemDB() + stateDB := blockDB + sm.SaveState(stateDB, state) //for save height 1's validators info blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyAppConnCon, mempool, evpool) cs := NewConsensusState(thisConfig.Consensus, state, blockExec, blockStore, mempool, evpool) cs.SetLogger(log.TestingLogger().With("module", "consensus")) @@ -564,7 +585,7 @@ func randConsensusNet(nValidators int, testName string, tickerFunc func() Timeou vals := types.TM2PB.ValidatorUpdates(state.Validators) app.InitChain(abci.RequestInitChain{Validators: vals}) - css[i] = newConsensusStateWithConfig(thisConfig, state, privVals[i], app) + css[i] = newConsensusStateWithConfigAndBlockStore(thisConfig, state, privVals[i], app, stateDB) css[i].SetTimeoutTicker(tickerFunc()) css[i].SetLogger(logger.With("validator", i, "module", "consensus")) } @@ -576,12 +597,11 @@ func randConsensusNet(nValidators int, testName string, tickerFunc func() Timeou } // nPeers = nValidators + nNotValidator -func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerFunc func() TimeoutTicker, - appFunc func() abci.Application) ([]*ConsensusState, cleanupFunc) { - +func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerFunc func() TimeoutTicker, appFunc func(string) abci.Application) ([]*ConsensusState, *types.GenesisDoc, *cfg.Config, cleanupFunc) { genDoc, privVals := randGenesisDoc(nValidators, false, testMinPower) css := make([]*ConsensusState, nPeers) logger := consensusLogger() + var peer0Config *cfg.Config configRootDirs := make([]string, 0, nPeers) for i := 0; i < nPeers; i++ { stateDB := dbm.NewMemDB() // each state needs its own db @@ -589,6 +609,9 @@ func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerF thisConfig := ResetConfig(fmt.Sprintf("%s_%d", testName, i)) configRootDirs = append(configRootDirs, thisConfig.RootDir) ensureDir(filepath.Dir(thisConfig.Consensus.WalFile()), 0700) // dir for wal + if i == 0 { + peer0Config = thisConfig + } var privVal types.PrivValidator if i < nValidators { privVal = privVals[i] @@ -605,15 +628,19 @@ func randConsensusNetWithPeers(nValidators, nPeers int, testName string, tickerF privVal = privval.GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) } - app := appFunc() + app := appFunc(path.Join(config.DBDir(), fmt.Sprintf("%s_%d", testName, i))) vals := types.TM2PB.ValidatorUpdates(state.Validators) + if _, ok := app.(*kvstore.PersistentKVStoreApplication); ok { + state.Version.Consensus.App = kvstore.ProtocolVersion //simulate handshake, receive app version. If don't do this, replay test will fail + } app.InitChain(abci.RequestInitChain{Validators: vals}) + //sm.SaveState(stateDB,state) //height 1's validatorsInfo already saved in LoadStateFromDBOrGenesisDoc above css[i] = newConsensusStateWithConfig(thisConfig, state, privVal, app) css[i].SetTimeoutTicker(tickerFunc()) css[i].SetLogger(logger.With("validator", i, "module", "consensus")) } - return css, func() { + return css, genDoc, peer0Config, func() { for _, dir := range configRootDirs { os.RemoveAll(dir) } @@ -719,3 +746,7 @@ func newPersistentKVStore() abci.Application { } return kvstore.NewPersistentKVStoreApplication(dir) } + +func newPersistentKVStoreWithPath(dbDir string) abci.Application { + return kvstore.NewPersistentKVStoreApplication(dbDir) +} diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index e7669b177..1baa92b89 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -11,6 +11,7 @@ import ( "github.com/tendermint/tendermint/abci/example/code" abci "github.com/tendermint/tendermint/abci/types" + dbm "github.com/tendermint/tendermint/libs/db" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -106,7 +107,9 @@ func deliverTxsRange(cs *ConsensusState, start, end int) { func TestMempoolTxConcurrentWithCommit(t *testing.T) { state, privVals := randGenesisState(1, false, 10) - cs := newConsensusState(state, privVals[0], NewCounterApplication()) + blockDB := dbm.NewMemDB() + cs := newConsensusStateWithConfigAndBlockStore(config, state, privVals[0], NewCounterApplication(), blockDB) + sm.SaveState(blockDB, state) height, round := cs.Height, cs.Round newBlockCh := subscribe(cs.eventBus, types.EventQueryNewBlock) @@ -129,7 +132,9 @@ func TestMempoolTxConcurrentWithCommit(t *testing.T) { func TestMempoolRmBadTx(t *testing.T) { state, privVals := randGenesisState(1, false, 10) app := NewCounterApplication() - cs := newConsensusState(state, privVals[0], app) + blockDB := dbm.NewMemDB() + cs := newConsensusStateWithConfigAndBlockStore(config, state, privVals[0], app, blockDB) + sm.SaveState(blockDB, state) // increment the counter by 1 txBytes := make([]byte, 8) diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index dfd6f2508..bf58a8eeb 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -51,6 +51,10 @@ func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ( blocksSub, err := eventBuses[i].Subscribe(context.Background(), testSubscriber, types.EventQueryNewBlock) require.NoError(t, err) blocksSubs = append(blocksSubs, blocksSub) + + if css[i].state.LastBlockHeight == 0 { //simulate handle initChain in handshake + sm.SaveState(css[i].blockExec.DB(), css[i].state) + } } // make connected switches and start all reactors p2p.MakeConnectedSwitches(config.P2P, N, func(i int, s *p2p.Switch) *p2p.Switch { @@ -329,7 +333,8 @@ func TestReactorVotingPowerChange(t *testing.T) { func TestReactorValidatorSetChanges(t *testing.T) { nPeers := 7 nVals := 4 - css, cleanup := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentKVStore) + css, _, _, cleanup := randConsensusNetWithPeers(nVals, nPeers, "consensus_val_set_changes_test", newMockTickerFunc(true), newPersistentKVStoreWithPath) + defer cleanup() logger := log.TestingLogger() diff --git a/consensus/replay.go b/consensus/replay.go index 38ed79fcf..cc1344eba 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -258,8 +258,10 @@ func (h *Handshaker) Handshake(proxyApp proxy.AppConns) error { ) // Set AppVersion on the state. - h.initialState.Version.Consensus.App = version.Protocol(res.AppVersion) - sm.SaveState(h.stateDB, h.initialState) + if h.initialState.Version.Consensus.App != version.Protocol(res.AppVersion) { + h.initialState.Version.Consensus.App = version.Protocol(res.AppVersion) + sm.SaveState(h.stateDB, h.initialState) + } // Replay blocks up to the latest in the blockstore. _, err = h.ReplayBlocks(h.initialState, appHash, blockHeight, proxyApp) @@ -421,13 +423,12 @@ func (h *Handshaker) replayBlocks(state sm.State, proxyApp proxy.AppConns, appBl for i := appBlockHeight + 1; i <= finalBlock; i++ { h.logger.Info("Applying block", "height", i) block := h.store.LoadBlock(i) - // Extra check to ensure the app was not changed in a way it shouldn't have. if len(appHash) > 0 { assertAppHashEqualsOneFromBlock(appHash, block) } - appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, state.LastValidators, h.stateDB) + appHash, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, h.logger, h.stateDB) if err != nil { return nil, err } @@ -517,6 +518,9 @@ type mockProxyApp struct { func (mock *mockProxyApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { r := mock.abciResponses.DeliverTx[mock.txCount] mock.txCount++ + if r == nil { //it could be nil because of amino unMarshall, it will cause an empty ResponseDeliverTx to become nil + return abci.ResponseDeliverTx{} + } return *r } diff --git a/consensus/replay_test.go b/consensus/replay_test.go index b084cef4e..6ed361905 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -15,6 +15,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "sort" + "github.com/tendermint/tendermint/abci/example/kvstore" abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" @@ -140,10 +142,10 @@ LOOP: // create consensus state from a clean slate logger := log.NewNopLogger() - stateDB := dbm.NewMemDB() + blockDB := dbm.NewMemDB() + stateDB := blockDB state, _ := sm.MakeGenesisStateFromFile(consensusReplayConfig.GenesisFile()) privValidator := loadPrivValidator(consensusReplayConfig) - blockDB := dbm.NewMemDB() cs := newConsensusStateWithConfigAndBlockStore(consensusReplayConfig, state, privValidator, kvstore.NewKVStoreApplication(), blockDB) cs.SetLogger(logger) @@ -262,7 +264,13 @@ func (w *crashingWAL) Stop() error { return w.next.Stop() } func (w *crashingWAL) Wait() { w.next.Wait() } //------------------------------------------------------------------------------------------ -// Handshake Tests +type testSim struct { + GenesisState sm.State + Config *cfg.Config + Chain []*types.Block + Commits []*types.Commit + CleanupFunc cleanupFunc +} const ( numBlocks = 6 @@ -271,6 +279,8 @@ const ( var ( mempool = sm.MockMempool{} evpool = sm.MockEvidencePool{} + + sim testSim ) //--------------------------------------- @@ -281,46 +291,285 @@ var ( // 2 - save block and committed but state is behind var modes = []uint{0, 1, 2} +// This is actually not a test, it's for storing validator change tx data for testHandshakeReplay +func TestSimulateValidatorsChange(t *testing.T) { + nPeers := 7 + nVals := 4 + css, genDoc, config, cleanup := randConsensusNetWithPeers(nVals, nPeers, "replay_test", newMockTickerFunc(true), newPersistentKVStoreWithPath) + sim.Config = config + sim.GenesisState, _ = sm.MakeGenesisState(genDoc) + sim.CleanupFunc = cleanup + + partSize := types.BlockPartSizeBytes + + newRoundCh := subscribe(css[0].eventBus, types.EventQueryNewRound) + proposalCh := subscribe(css[0].eventBus, types.EventQueryCompleteProposal) + + vss := make([]*validatorStub, nPeers) + for i := 0; i < nPeers; i++ { + vss[i] = NewValidatorStub(css[i].privValidator, i) + } + height, round := css[0].Height, css[0].Round + // start the machine + startTestRound(css[0], height, round) + incrementHeight(vss...) + ensureNewRound(newRoundCh, height, 0) + ensureNewProposal(proposalCh, height, round) + rs := css[0].GetRoundState() + signAddVotes(css[0], types.PrecommitType, rs.ProposalBlock.Hash(), rs.ProposalBlockParts.Header(), vss[1:nVals]...) + ensureNewRound(newRoundCh, height+1, 0) + + //height 2 + height++ + incrementHeight(vss...) + newValidatorPubKey1 := css[nVals].privValidator.GetPubKey() + valPubKey1ABCI := types.TM2PB.PubKey(newValidatorPubKey1) + newValidatorTx1 := kvstore.MakeValSetChangeTx(valPubKey1ABCI, testMinPower) + err := assertMempool(css[0].txNotifier).CheckTx(newValidatorTx1, nil) + assert.Nil(t, err) + propBlock, _ := css[0].createProposalBlock() //changeProposer(t, cs1, vs2) + propBlockParts := propBlock.MakePartSet(partSize) + blockID := types.BlockID{Hash: propBlock.Hash(), PartsHeader: propBlockParts.Header()} + proposal := types.NewProposal(vss[1].Height, round, -1, blockID) + if err := vss[1].SignProposal(config.ChainID(), proposal); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + + // set the proposal block + if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil { + t.Fatal(err) + } + ensureNewProposal(proposalCh, height, round) + rs = css[0].GetRoundState() + signAddVotes(css[0], types.PrecommitType, rs.ProposalBlock.Hash(), rs.ProposalBlockParts.Header(), vss[1:nVals]...) + ensureNewRound(newRoundCh, height+1, 0) + + //height 3 + height++ + incrementHeight(vss...) + updateValidatorPubKey1 := css[nVals].privValidator.GetPubKey() + updatePubKey1ABCI := types.TM2PB.PubKey(updateValidatorPubKey1) + updateValidatorTx1 := kvstore.MakeValSetChangeTx(updatePubKey1ABCI, 25) + err = assertMempool(css[0].txNotifier).CheckTx(updateValidatorTx1, nil) + assert.Nil(t, err) + propBlock, _ = css[0].createProposalBlock() //changeProposer(t, cs1, vs2) + propBlockParts = propBlock.MakePartSet(partSize) + blockID = types.BlockID{Hash: propBlock.Hash(), PartsHeader: propBlockParts.Header()} + proposal = types.NewProposal(vss[2].Height, round, -1, blockID) + if err := vss[2].SignProposal(config.ChainID(), proposal); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + + // set the proposal block + if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil { + t.Fatal(err) + } + ensureNewProposal(proposalCh, height, round) + rs = css[0].GetRoundState() + signAddVotes(css[0], types.PrecommitType, rs.ProposalBlock.Hash(), rs.ProposalBlockParts.Header(), vss[1:nVals]...) + ensureNewRound(newRoundCh, height+1, 0) + + //height 4 + height++ + incrementHeight(vss...) + newValidatorPubKey2 := css[nVals+1].privValidator.GetPubKey() + newVal2ABCI := types.TM2PB.PubKey(newValidatorPubKey2) + newValidatorTx2 := kvstore.MakeValSetChangeTx(newVal2ABCI, testMinPower) + err = assertMempool(css[0].txNotifier).CheckTx(newValidatorTx2, nil) + assert.Nil(t, err) + newValidatorPubKey3 := css[nVals+2].privValidator.GetPubKey() + newVal3ABCI := types.TM2PB.PubKey(newValidatorPubKey3) + newValidatorTx3 := kvstore.MakeValSetChangeTx(newVal3ABCI, testMinPower) + err = assertMempool(css[0].txNotifier).CheckTx(newValidatorTx3, nil) + assert.Nil(t, err) + propBlock, _ = css[0].createProposalBlock() //changeProposer(t, cs1, vs2) + propBlockParts = propBlock.MakePartSet(partSize) + blockID = types.BlockID{Hash: propBlock.Hash(), PartsHeader: propBlockParts.Header()} + newVss := make([]*validatorStub, nVals+1) + copy(newVss, vss[:nVals+1]) + sort.Sort(ValidatorStubsByAddress(newVss)) + selfIndex := 0 + for i, vs := range newVss { + if vs.GetPubKey().Equals(css[0].privValidator.GetPubKey()) { + selfIndex = i + break + } + } + + proposal = types.NewProposal(vss[3].Height, round, -1, blockID) + if err := vss[3].SignProposal(config.ChainID(), proposal); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + + // set the proposal block + if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil { + t.Fatal(err) + } + ensureNewProposal(proposalCh, height, round) + + removeValidatorTx2 := kvstore.MakeValSetChangeTx(newVal2ABCI, 0) + err = assertMempool(css[0].txNotifier).CheckTx(removeValidatorTx2, nil) + assert.Nil(t, err) + + rs = css[0].GetRoundState() + for i := 0; i < nVals+1; i++ { + if i == selfIndex { + continue + } + signAddVotes(css[0], types.PrecommitType, rs.ProposalBlock.Hash(), rs.ProposalBlockParts.Header(), newVss[i]) + } + + ensureNewRound(newRoundCh, height+1, 0) + + //height 5 + height++ + incrementHeight(vss...) + ensureNewProposal(proposalCh, height, round) + rs = css[0].GetRoundState() + for i := 0; i < nVals+1; i++ { + if i == selfIndex { + continue + } + signAddVotes(css[0], types.PrecommitType, rs.ProposalBlock.Hash(), rs.ProposalBlockParts.Header(), newVss[i]) + } + ensureNewRound(newRoundCh, height+1, 0) + + //height 6 + height++ + incrementHeight(vss...) + removeValidatorTx3 := kvstore.MakeValSetChangeTx(newVal3ABCI, 0) + err = assertMempool(css[0].txNotifier).CheckTx(removeValidatorTx3, nil) + assert.Nil(t, err) + propBlock, _ = css[0].createProposalBlock() //changeProposer(t, cs1, vs2) + propBlockParts = propBlock.MakePartSet(partSize) + blockID = types.BlockID{Hash: propBlock.Hash(), PartsHeader: propBlockParts.Header()} + newVss = make([]*validatorStub, nVals+3) + copy(newVss, vss[:nVals+3]) + sort.Sort(ValidatorStubsByAddress(newVss)) + for i, vs := range newVss { + if vs.GetPubKey().Equals(css[0].privValidator.GetPubKey()) { + selfIndex = i + break + } + } + proposal = types.NewProposal(vss[1].Height, round, -1, blockID) + if err := vss[1].SignProposal(config.ChainID(), proposal); err != nil { + t.Fatal("failed to sign bad proposal", err) + } + + // set the proposal block + if err := css[0].SetProposalAndBlock(proposal, propBlock, propBlockParts, "some peer"); err != nil { + t.Fatal(err) + } + ensureNewProposal(proposalCh, height, round) + rs = css[0].GetRoundState() + for i := 0; i < nVals+3; i++ { + if i == selfIndex { + continue + } + signAddVotes(css[0], types.PrecommitType, rs.ProposalBlock.Hash(), rs.ProposalBlockParts.Header(), newVss[i]) + } + ensureNewRound(newRoundCh, height+1, 0) + + sim.Chain = make([]*types.Block, 0) + sim.Commits = make([]*types.Commit, 0) + for i := 1; i <= numBlocks; i++ { + sim.Chain = append(sim.Chain, css[0].blockStore.LoadBlock(int64(i))) + sim.Commits = append(sim.Commits, css[0].blockStore.LoadBlockCommit(int64(i))) + } +} + // Sync from scratch func TestHandshakeReplayAll(t *testing.T) { - for i, m := range modes { - config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) - defer os.RemoveAll(config.RootDir) - - testHandshakeReplay(t, config, 0, m) + for _, m := range modes { + testHandshakeReplay(t, config, 0, m, false) + } + for _, m := range modes { + testHandshakeReplay(t, config, 0, m, true) } } // Sync many, not from scratch func TestHandshakeReplaySome(t *testing.T) { - for i, m := range modes { - config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) - defer os.RemoveAll(config.RootDir) - - testHandshakeReplay(t, config, 1, m) + for _, m := range modes { + testHandshakeReplay(t, config, 1, m, false) + } + for _, m := range modes { + testHandshakeReplay(t, config, 1, m, true) } } // Sync from lagging by one func TestHandshakeReplayOne(t *testing.T) { - for i, m := range modes { - config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) - defer os.RemoveAll(config.RootDir) - - testHandshakeReplay(t, config, numBlocks-1, m) + for _, m := range modes { + testHandshakeReplay(t, config, numBlocks-1, m, false) + } + for _, m := range modes { + testHandshakeReplay(t, config, numBlocks-1, m, true) } } // Sync from caught up func TestHandshakeReplayNone(t *testing.T) { - for i, m := range modes { - config := ResetConfig(fmt.Sprintf("%s_%v", t.Name(), i)) - defer os.RemoveAll(config.RootDir) - - testHandshakeReplay(t, config, numBlocks, m) + for _, m := range modes { + testHandshakeReplay(t, config, numBlocks, m, false) + } + for _, m := range modes { + testHandshakeReplay(t, config, numBlocks, m, true) } } +// Test mockProxyApp should not panic when app return ABCIResponses with some empty ResponseDeliverTx +func TestMockProxyApp(t *testing.T) { + sim.CleanupFunc() //clean the test env created in TestSimulateValidatorsChange + logger := log.TestingLogger() + var validTxs, invalidTxs = 0, 0 + txIndex := 0 + + assert.NotPanics(t, func() { + abciResWithEmptyDeliverTx := new(sm.ABCIResponses) + abciResWithEmptyDeliverTx.DeliverTx = make([]*abci.ResponseDeliverTx, 0) + abciResWithEmptyDeliverTx.DeliverTx = append(abciResWithEmptyDeliverTx.DeliverTx, &abci.ResponseDeliverTx{}) + + // called when saveABCIResponses: + bytes := cdc.MustMarshalBinaryBare(abciResWithEmptyDeliverTx) + loadedAbciRes := new(sm.ABCIResponses) + + // this also happens sm.LoadABCIResponses + err := cdc.UnmarshalBinaryBare(bytes, loadedAbciRes) + require.NoError(t, err) + + mock := newMockProxyApp([]byte("mock_hash"), loadedAbciRes) + + abciRes := new(sm.ABCIResponses) + abciRes.DeliverTx = make([]*abci.ResponseDeliverTx, len(loadedAbciRes.DeliverTx)) + // Execute transactions and get hash. + proxyCb := func(req *abci.Request, res *abci.Response) { + switch r := res.Value.(type) { + case *abci.Response_DeliverTx: + // TODO: make use of res.Log + // TODO: make use of this info + // Blocks may include invalid txs. + txRes := r.DeliverTx + if txRes.Code == abci.CodeTypeOK { + validTxs++ + } else { + logger.Debug("Invalid tx", "code", txRes.Code, "log", txRes.Log) + invalidTxs++ + } + abciRes.DeliverTx[txIndex] = txRes + txIndex++ + } + } + mock.SetResponseCallback(proxyCb) + + someTx := []byte("tx") + mock.DeliverTxAsync(someTx) + }) + assert.True(t, validTxs == 1) + assert.True(t, invalidTxs == 0) +} + func tempWALWithData(data []byte) string { walFile, err := ioutil.TempFile("", "wal") if err != nil { @@ -336,54 +585,68 @@ func tempWALWithData(data []byte) string { return walFile.Name() } -// Make some blocks. Start a fresh app and apply nBlocks blocks. Then restart -// the app and sync it up with the remaining blocks. -func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uint) { - walBody, err := WALWithNBlocks(t, numBlocks) - require.NoError(t, err) - walFile := tempWALWithData(walBody) - config.Consensus.SetWalFile(walFile) - - privVal := privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) +// Make some blocks. Start a fresh app and apply nBlocks blocks. Then restart the app and sync it up with the remaining blocks +func testHandshakeReplay(t *testing.T, config *cfg.Config, nBlocks int, mode uint, testValidatorsChange bool) { + var chain []*types.Block + var commits []*types.Commit + var store *mockBlockStore + var stateDB dbm.DB + var genisisState sm.State + if testValidatorsChange { + testConfig := ResetConfig(fmt.Sprintf("%s_%v_m", t.Name(), mode)) + defer os.RemoveAll(testConfig.RootDir) + stateDB = dbm.NewMemDB() + genisisState = sim.GenesisState + config = sim.Config + chain = sim.Chain + commits = sim.Commits + store = newMockBlockStore(config, genisisState.ConsensusParams) + } else { //test single node + testConfig := ResetConfig(fmt.Sprintf("%s_%v_s", t.Name(), mode)) + defer os.RemoveAll(testConfig.RootDir) + walBody, err := WALWithNBlocks(t, numBlocks) + require.NoError(t, err) + walFile := tempWALWithData(walBody) + config.Consensus.SetWalFile(walFile) - wal, err := NewWAL(walFile) - require.NoError(t, err) - wal.SetLogger(log.TestingLogger()) - err = wal.Start() - require.NoError(t, err) - defer wal.Stop() + privVal := privval.LoadFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()) - chain, commits, err := makeBlockchainFromWAL(wal) - require.NoError(t, err) + wal, err := NewWAL(walFile) + require.NoError(t, err) + wal.SetLogger(log.TestingLogger()) + err = wal.Start() + require.NoError(t, err) + defer wal.Stop() - stateDB, state, store := stateAndStore(config, privVal.GetPubKey(), kvstore.ProtocolVersion) + chain, commits, err = makeBlockchainFromWAL(wal) + require.NoError(t, err) + stateDB, genisisState, store = stateAndStore(config, privVal.GetPubKey(), kvstore.ProtocolVersion) + } store.chain = chain store.commits = commits + state := genisisState.Copy() // run the chain through state.ApplyBlock to build up the tendermint state - clientCreator := proxy.NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), "1"))) - proxyApp := proxy.NewAppConns(clientCreator) - err = proxyApp.Start() - require.NoError(t, err) - state = buildTMStateFromChain(config, stateDB, state, chain, proxyApp, mode) - proxyApp.Stop() + state = buildTMStateFromChain(config, stateDB, state, chain, nBlocks, mode) latestAppHash := state.AppHash // make a new client creator - kvstoreApp := kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), "2")) + kvstoreApp := kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), fmt.Sprintf("replay_test_%d_%d_a", nBlocks, mode))) + clientCreator2 := proxy.NewLocalClientCreator(kvstoreApp) if nBlocks > 0 { // run nBlocks against a new client to build up the app state. // use a throwaway tendermint state proxyApp := proxy.NewAppConns(clientCreator2) - stateDB, state, _ := stateAndStore(config, privVal.GetPubKey(), kvstore.ProtocolVersion) - buildAppStateFromChain(proxyApp, stateDB, state, chain, nBlocks, mode) + stateDB1 := dbm.NewMemDB() + sm.SaveState(stateDB1, genisisState) + buildAppStateFromChain(proxyApp, stateDB1, genisisState, chain, nBlocks, mode) } // now start the app using the handshake - it should sync genDoc, _ := sm.MakeGenesisDocFromFile(config.GenesisFile()) handshaker := NewHandshaker(stateDB, state, store, genDoc) - proxyApp = proxy.NewAppConns(clientCreator2) + proxyApp := proxy.NewAppConns(clientCreator2) if err := proxyApp.Start(); err != nil { t.Fatalf("Error starting proxy app connections: %v", err) } @@ -435,12 +698,14 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, stateDB dbm.DB, } defer proxyApp.Stop() + state.Version.Consensus.App = kvstore.ProtocolVersion //simulate handshake, receive app version validators := types.TM2PB.ValidatorUpdates(state.Validators) if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{ Validators: validators, }); err != nil { panic(err) } + sm.SaveState(stateDB, state) //save height 1's validatorsInfo switch mode { case 0: @@ -463,15 +728,23 @@ func buildAppStateFromChain(proxyApp proxy.AppConns, stateDB dbm.DB, } -func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, - chain []*types.Block, proxyApp proxy.AppConns, mode uint) sm.State { +func buildTMStateFromChain(config *cfg.Config, stateDB dbm.DB, state sm.State, chain []*types.Block, nBlocks int, mode uint) sm.State { + // run the whole chain against this client to build up the tendermint state + clientCreator := proxy.NewLocalClientCreator(kvstore.NewPersistentKVStoreApplication(filepath.Join(config.DBDir(), fmt.Sprintf("replay_test_%d_%d_t", nBlocks, mode)))) + proxyApp := proxy.NewAppConns(clientCreator) + if err := proxyApp.Start(); err != nil { + panic(err) + } + defer proxyApp.Stop() + state.Version.Consensus.App = kvstore.ProtocolVersion //simulate handshake, receive app version validators := types.TM2PB.ValidatorUpdates(state.Validators) if _, err := proxyApp.Consensus().InitChainSync(abci.RequestInitChain{ Validators: validators, }); err != nil { panic(err) } + sm.SaveState(stateDB, state) //save height 1's validatorsInfo switch mode { case 0: @@ -743,6 +1016,7 @@ func stateAndStore(config *cfg.Config, pubKey crypto.PubKey, appVersion version. state, _ := sm.MakeGenesisStateFromFile(config.GenesisFile()) state.Version.Consensus.App = appVersion store := newMockBlockStore(config, state.ConsensusParams) + sm.SaveState(stateDB, state) return stateDB, state, store } diff --git a/consensus/state_test.go b/consensus/state_test.go index 67b5cfbdc..87e351dc8 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -239,7 +239,7 @@ func TestStateFullRound1(t *testing.T) { cs.SetEventBus(eventBus) eventBus.Start() - voteCh := subscribe(cs.eventBus, types.EventQueryVote) + voteCh := subscribeUnBuffered(cs.eventBus, types.EventQueryVote) propCh := subscribe(cs.eventBus, types.EventQueryCompleteProposal) newRoundCh := subscribe(cs.eventBus, types.EventQueryNewRound) @@ -267,7 +267,7 @@ func TestStateFullRoundNil(t *testing.T) { cs, vss := randConsensusState(1) height, round := cs.Height, cs.Round - voteCh := subscribe(cs.eventBus, types.EventQueryVote) + voteCh := subscribeUnBuffered(cs.eventBus, types.EventQueryVote) cs.enterPrevote(height, round) cs.startRoutines(4) @@ -286,7 +286,7 @@ func TestStateFullRound2(t *testing.T) { vs2 := vss[1] height, round := cs1.Height, cs1.Round - voteCh := subscribe(cs1.eventBus, types.EventQueryVote) + voteCh := subscribeUnBuffered(cs1.eventBus, types.EventQueryVote) newBlockCh := subscribe(cs1.eventBus, types.EventQueryNewBlock) // start round and wait for propose and prevote @@ -330,7 +330,7 @@ func TestStateLockNoPOL(t *testing.T) { timeoutProposeCh := subscribe(cs1.eventBus, types.EventQueryTimeoutPropose) timeoutWaitCh := subscribe(cs1.eventBus, types.EventQueryTimeoutWait) - voteCh := subscribe(cs1.eventBus, types.EventQueryVote) + voteCh := subscribeUnBuffered(cs1.eventBus, types.EventQueryVote) proposalCh := subscribe(cs1.eventBus, types.EventQueryCompleteProposal) newRoundCh := subscribe(cs1.eventBus, types.EventQueryNewRound) @@ -1623,3 +1623,12 @@ func subscribe(eventBus *types.EventBus, q tmpubsub.Query) <-chan tmpubsub.Messa } return sub.Out() } + +// subscribe subscribes test client to the given query and returns a channel with cap = 0. +func subscribeUnBuffered(eventBus *types.EventBus, q tmpubsub.Query) <-chan tmpubsub.Message { + sub, err := eventBus.SubscribeUnbuffered(context.Background(), testSubscriber, q) + if err != nil { + panic(fmt.Sprintf("failed to subscribe %s to %v", testSubscriber, q)) + } + return sub.Out() +} diff --git a/consensus/wal_generator.go b/consensus/wal_generator.go index 1a4cfb9ff..c007a21cf 100644 --- a/consensus/wal_generator.go +++ b/consensus/wal_generator.go @@ -45,13 +45,14 @@ func WALGenerateNBlocks(t *testing.T, wr io.Writer, numBlocks int) (err error) { if err != nil { return errors.Wrap(err, "failed to read genesis file") } - stateDB := db.NewMemDB() blockStoreDB := db.NewMemDB() + stateDB := blockStoreDB state, err := sm.MakeGenesisState(genDoc) if err != nil { return errors.Wrap(err, "failed to make genesis state") } state.Version.Consensus.App = kvstore.ProtocolVersion + sm.SaveState(stateDB, state) blockStore := bc.NewBlockStore(blockStoreDB) proxyApp := proxy.NewAppConns(proxy.NewLocalClientCreator(app)) proxyApp.SetLogger(logger.With("module", "proxy")) diff --git a/libs/pubsub/pubsub_test.go b/libs/pubsub/pubsub_test.go index 884477563..74af431f8 100644 --- a/libs/pubsub/pubsub_test.go +++ b/libs/pubsub/pubsub_test.go @@ -46,13 +46,16 @@ func TestSubscribe(t *testing.T) { err = s.Publish(ctx, "Asylum") assert.NoError(t, err) + + err = s.Publish(ctx, "Ivan") + assert.NoError(t, err) }() select { case <-published: assertReceive(t, "Quicksilver", subscription.Out()) assertCancelled(t, subscription, pubsub.ErrOutOfCapacity) - case <-time.After(100 * time.Millisecond): + case <-time.After(3 * time.Second): t.Fatal("Expected Publish(Asylum) not to block") } } @@ -101,7 +104,7 @@ func TestSubscribeUnbuffered(t *testing.T) { select { case <-published: t.Fatal("Expected Publish(Darkhawk) to block") - case <-time.After(100 * time.Millisecond): + case <-time.After(3 * time.Second): assertReceive(t, "Ultron", subscription.Out()) assertReceive(t, "Darkhawk", subscription.Out()) } diff --git a/state/execution.go b/state/execution.go index 3a11ecca4..245325525 100644 --- a/state/execution.go +++ b/state/execution.go @@ -66,6 +66,10 @@ func NewBlockExecutor(db dbm.DB, logger log.Logger, proxyApp proxy.AppConnConsen return res } +func (blockExec *BlockExecutor) DB() dbm.DB { + return blockExec.db +} + // SetEventBus - sets the event bus for publishing block related events. // If not called, it defaults to types.NopEventBus. func (blockExec *BlockExecutor) SetEventBus(eventBus types.BlockEventPublisher) { @@ -116,7 +120,7 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b } startTime := time.Now().UnixNano() - abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, state.LastValidators, blockExec.db) + abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, blockExec.db) endTime := time.Now().UnixNano() blockExec.metrics.BlockProcessingTime.Observe(float64(endTime-startTime) / 1000000) if err != nil { @@ -233,7 +237,6 @@ func execBlockOnProxyApp( logger log.Logger, proxyAppConn proxy.AppConnConsensus, block *types.Block, - lastValSet *types.ValidatorSet, stateDB dbm.DB, ) (*ABCIResponses, error) { var validTxs, invalidTxs = 0, 0 @@ -261,7 +264,7 @@ func execBlockOnProxyApp( } proxyAppConn.SetResponseCallback(proxyCb) - commitInfo, byzVals := getBeginBlockValidatorInfo(block, lastValSet, stateDB) + commitInfo, byzVals := getBeginBlockValidatorInfo(block, stateDB) // Begin block var err error @@ -296,22 +299,31 @@ func execBlockOnProxyApp( return abciResponses, nil } -func getBeginBlockValidatorInfo(block *types.Block, lastValSet *types.ValidatorSet, stateDB dbm.DB) (abci.LastCommitInfo, []abci.Evidence) { - - // Sanity check that commit length matches validator set size - - // only applies after first block +func getBeginBlockValidatorInfo(block *types.Block, stateDB dbm.DB) (abci.LastCommitInfo, []abci.Evidence) { + voteInfos := make([]abci.VoteInfo, block.LastCommit.Size()) + byzVals := make([]abci.Evidence, len(block.Evidence.Evidence)) + var lastValSet *types.ValidatorSet + var err error if block.Height > 1 { - precommitLen := len(block.LastCommit.Precommits) + lastValSet, err = LoadValidators(stateDB, block.Height-1) + if err != nil { + panic(err) // shouldn't happen + } + + // Sanity check that commit length matches validator set size - + // only applies after first block + + precommitLen := block.LastCommit.Size() valSetLen := len(lastValSet.Validators) if precommitLen != valSetLen { // sanity check panic(fmt.Sprintf("precommit length (%d) doesn't match valset length (%d) at height %d\n\n%v\n\n%v", precommitLen, valSetLen, block.Height, block.LastCommit.Precommits, lastValSet.Validators)) } + } else { + lastValSet = types.NewValidatorSet(nil) } - // Collect the vote info (list of validators and whether or not they signed). - voteInfos := make([]abci.VoteInfo, len(lastValSet.Validators)) for i, val := range lastValSet.Validators { var vote *types.CommitSig if i < len(block.LastCommit.Precommits) { @@ -324,12 +336,6 @@ func getBeginBlockValidatorInfo(block *types.Block, lastValSet *types.ValidatorS voteInfos[i] = voteInfo } - commitInfo := abci.LastCommitInfo{ - Round: int32(block.LastCommit.Round()), - Votes: voteInfos, - } - - byzVals := make([]abci.Evidence, len(block.Evidence.Evidence)) for i, ev := range block.Evidence.Evidence { // We need the validator set. We already did this in validateBlock. // TODO: Should we instead cache the valset in the evidence itself and add @@ -341,6 +347,10 @@ func getBeginBlockValidatorInfo(block *types.Block, lastValSet *types.ValidatorS byzVals[i] = types.TM2PB.Evidence(ev, valset, block.Time) } + commitInfo := abci.LastCommitInfo{ + Round: int32(block.LastCommit.Round()), + Votes: voteInfos, + } return commitInfo, byzVals } @@ -469,10 +479,9 @@ func ExecCommitBlock( appConnConsensus proxy.AppConnConsensus, block *types.Block, logger log.Logger, - lastValSet *types.ValidatorSet, stateDB dbm.DB, ) ([]byte, error) { - _, err := execBlockOnProxyApp(logger, appConnConsensus, block, lastValSet, stateDB) + _, err := execBlockOnProxyApp(logger, appConnConsensus, block, stateDB) if err != nil { logger.Error("Error executing block on proxy app", "height", block.Height, "err", err) return nil, err diff --git a/state/execution_test.go b/state/execution_test.go index a9fdfe270..092fbd68f 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -85,7 +85,7 @@ func TestBeginBlockValidators(t *testing.T) { // block for height 2 block, _ := state.MakeBlock(2, makeTxs(2), lastCommit, nil, state.Validators.GetProposer().Address) - _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), state.Validators, stateDB) + _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) require.Nil(t, err, tc.desc) // -> app receives a list of validators with a bool indicating if they signed @@ -146,7 +146,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { block, _ := state.MakeBlock(10, makeTxs(2), lastCommit, nil, state.Validators.GetProposer().Address) block.Time = now block.Evidence.Evidence = tc.evidence - _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), state.Validators, stateDB) + _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) require.Nil(t, err, tc.desc) // -> app must receive an index of the byzantine validator From 5df6cf563ad201ac3d4214283efc3765777d0c5c Mon Sep 17 00:00:00 2001 From: Ivan Kushmantsev Date: Thu, 2 May 2019 10:09:56 +0400 Subject: [PATCH 017/211] p2p: session should terminate on nonce wrapping (#3531) (#3609) Refs #3531 --- CHANGELOG_PENDING.md | 5 +++-- p2p/conn/secret_connection.go | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f777c6e06..8f162e072 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,13 +20,14 @@ ### IMPROVEMENTS: - [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC -- [cli] \#3606 (https://github.com/tendermint/tendermint/issues/3585) Add option to not clear address book with unsafe reset (@climber73) +- [cli] \#3585 Add option to not clear address book with unsafe reset (@climber73) - [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `-config=` option to `testnet` cmd (@gregdhill) - [cs/replay] \#3460 check appHash for each block +- [p2p] \#3531 Terminate session on nonce wrapping (@climber73) ### BUG FIXES: - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode to 16 (as a result, the node will stop retrying after a 35 hours time window) - [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests -- [consensus] \#3067 getBeginBlockValidatorInfo loads validators from stateDB instead of state +- [consensus] \#3067 getBeginBlockValidatorInfo loads validators from stateDB instead of state (@james-ray) - [pex] \#3603 Dial seeds when addrbook needs more addresses (@defunctzombie) diff --git a/p2p/conn/secret_connection.go b/p2p/conn/secret_connection.go index 36d6ee1bb..7f76ac800 100644 --- a/p2p/conn/secret_connection.go +++ b/p2p/conn/secret_connection.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "errors" "io" + "math" "net" "sync" "time" @@ -439,6 +440,11 @@ func shareAuthSignature(sc *SecretConnection, pubKey crypto.PubKey, signature [] // (little-endian in nonce[4:]). func incrNonce(nonce *[aeadNonceSize]byte) { counter := binary.LittleEndian.Uint64(nonce[4:]) + if counter == math.MaxUint64 { + // Terminates the session and makes sure the nonce would not re-used. + // See https://github.com/tendermint/tendermint/issues/3531 + panic("can't increase nonce without overflow") + } counter++ binary.LittleEndian.PutUint64(nonce[4:], counter) } From 9926ae768ee3e4ecb200540ac6a74c074516274c Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 2 May 2019 11:53:33 +0400 Subject: [PATCH 018/211] abci/types: update comment (#3612) Fixes #3607 --- abci/types/types.pb.go | 86 +++++++++++++++++++++--------------------- abci/types/types.proto | 2 +- 2 files changed, 44 insertions(+), 44 deletions(-) diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index b09213a5f..7bf50f330 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -61,7 +61,7 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{0} + return fileDescriptor_types_7e896a7c04915591, []int{0} } func (m *Request) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -483,7 +483,7 @@ func (m *RequestEcho) Reset() { *m = RequestEcho{} } func (m *RequestEcho) String() string { return proto.CompactTextString(m) } func (*RequestEcho) ProtoMessage() {} func (*RequestEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{1} + return fileDescriptor_types_7e896a7c04915591, []int{1} } func (m *RequestEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -529,7 +529,7 @@ func (m *RequestFlush) Reset() { *m = RequestFlush{} } func (m *RequestFlush) String() string { return proto.CompactTextString(m) } func (*RequestFlush) ProtoMessage() {} func (*RequestFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{2} + return fileDescriptor_types_7e896a7c04915591, []int{2} } func (m *RequestFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -571,7 +571,7 @@ func (m *RequestInfo) Reset() { *m = RequestInfo{} } func (m *RequestInfo) String() string { return proto.CompactTextString(m) } func (*RequestInfo) ProtoMessage() {} func (*RequestInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{3} + return fileDescriptor_types_7e896a7c04915591, []int{3} } func (m *RequestInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -634,7 +634,7 @@ func (m *RequestSetOption) Reset() { *m = RequestSetOption{} } func (m *RequestSetOption) String() string { return proto.CompactTextString(m) } func (*RequestSetOption) ProtoMessage() {} func (*RequestSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{4} + return fileDescriptor_types_7e896a7c04915591, []int{4} } func (m *RequestSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -692,7 +692,7 @@ func (m *RequestInitChain) Reset() { *m = RequestInitChain{} } func (m *RequestInitChain) String() string { return proto.CompactTextString(m) } func (*RequestInitChain) ProtoMessage() {} func (*RequestInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{5} + return fileDescriptor_types_7e896a7c04915591, []int{5} } func (m *RequestInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -770,7 +770,7 @@ func (m *RequestQuery) Reset() { *m = RequestQuery{} } func (m *RequestQuery) String() string { return proto.CompactTextString(m) } func (*RequestQuery) ProtoMessage() {} func (*RequestQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{6} + return fileDescriptor_types_7e896a7c04915591, []int{6} } func (m *RequestQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -841,7 +841,7 @@ func (m *RequestBeginBlock) Reset() { *m = RequestBeginBlock{} } func (m *RequestBeginBlock) String() string { return proto.CompactTextString(m) } func (*RequestBeginBlock) ProtoMessage() {} func (*RequestBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{7} + return fileDescriptor_types_7e896a7c04915591, []int{7} } func (m *RequestBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -909,7 +909,7 @@ func (m *RequestCheckTx) Reset() { *m = RequestCheckTx{} } func (m *RequestCheckTx) String() string { return proto.CompactTextString(m) } func (*RequestCheckTx) ProtoMessage() {} func (*RequestCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{8} + return fileDescriptor_types_7e896a7c04915591, []int{8} } func (m *RequestCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -956,7 +956,7 @@ func (m *RequestDeliverTx) Reset() { *m = RequestDeliverTx{} } func (m *RequestDeliverTx) String() string { return proto.CompactTextString(m) } func (*RequestDeliverTx) ProtoMessage() {} func (*RequestDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{9} + return fileDescriptor_types_7e896a7c04915591, []int{9} } func (m *RequestDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1003,7 +1003,7 @@ func (m *RequestEndBlock) Reset() { *m = RequestEndBlock{} } func (m *RequestEndBlock) String() string { return proto.CompactTextString(m) } func (*RequestEndBlock) ProtoMessage() {} func (*RequestEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{10} + return fileDescriptor_types_7e896a7c04915591, []int{10} } func (m *RequestEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1049,7 +1049,7 @@ func (m *RequestCommit) Reset() { *m = RequestCommit{} } func (m *RequestCommit) String() string { return proto.CompactTextString(m) } func (*RequestCommit) ProtoMessage() {} func (*RequestCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{11} + return fileDescriptor_types_7e896a7c04915591, []int{11} } func (m *RequestCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1102,7 +1102,7 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{12} + return fileDescriptor_types_7e896a7c04915591, []int{12} } func (m *Response) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1555,7 +1555,7 @@ func (m *ResponseException) Reset() { *m = ResponseException{} } func (m *ResponseException) String() string { return proto.CompactTextString(m) } func (*ResponseException) ProtoMessage() {} func (*ResponseException) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{13} + return fileDescriptor_types_7e896a7c04915591, []int{13} } func (m *ResponseException) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1602,7 +1602,7 @@ func (m *ResponseEcho) Reset() { *m = ResponseEcho{} } func (m *ResponseEcho) String() string { return proto.CompactTextString(m) } func (*ResponseEcho) ProtoMessage() {} func (*ResponseEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{14} + return fileDescriptor_types_7e896a7c04915591, []int{14} } func (m *ResponseEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1648,7 +1648,7 @@ func (m *ResponseFlush) Reset() { *m = ResponseFlush{} } func (m *ResponseFlush) String() string { return proto.CompactTextString(m) } func (*ResponseFlush) ProtoMessage() {} func (*ResponseFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{15} + return fileDescriptor_types_7e896a7c04915591, []int{15} } func (m *ResponseFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1692,7 +1692,7 @@ func (m *ResponseInfo) Reset() { *m = ResponseInfo{} } func (m *ResponseInfo) String() string { return proto.CompactTextString(m) } func (*ResponseInfo) ProtoMessage() {} func (*ResponseInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{16} + return fileDescriptor_types_7e896a7c04915591, []int{16} } func (m *ResponseInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1771,7 +1771,7 @@ func (m *ResponseSetOption) Reset() { *m = ResponseSetOption{} } func (m *ResponseSetOption) String() string { return proto.CompactTextString(m) } func (*ResponseSetOption) ProtoMessage() {} func (*ResponseSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{17} + return fileDescriptor_types_7e896a7c04915591, []int{17} } func (m *ResponseSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1833,7 +1833,7 @@ func (m *ResponseInitChain) Reset() { *m = ResponseInitChain{} } func (m *ResponseInitChain) String() string { return proto.CompactTextString(m) } func (*ResponseInitChain) ProtoMessage() {} func (*ResponseInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{18} + return fileDescriptor_types_7e896a7c04915591, []int{18} } func (m *ResponseInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1896,7 +1896,7 @@ func (m *ResponseQuery) Reset() { *m = ResponseQuery{} } func (m *ResponseQuery) String() string { return proto.CompactTextString(m) } func (*ResponseQuery) ProtoMessage() {} func (*ResponseQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{19} + return fileDescriptor_types_7e896a7c04915591, []int{19} } func (m *ResponseQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1999,7 +1999,7 @@ func (m *ResponseBeginBlock) Reset() { *m = ResponseBeginBlock{} } func (m *ResponseBeginBlock) String() string { return proto.CompactTextString(m) } func (*ResponseBeginBlock) ProtoMessage() {} func (*ResponseBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{20} + return fileDescriptor_types_7e896a7c04915591, []int{20} } func (m *ResponseBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2053,7 +2053,7 @@ func (m *ResponseCheckTx) Reset() { *m = ResponseCheckTx{} } func (m *ResponseCheckTx) String() string { return proto.CompactTextString(m) } func (*ResponseCheckTx) ProtoMessage() {} func (*ResponseCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{21} + return fileDescriptor_types_7e896a7c04915591, []int{21} } func (m *ResponseCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2156,7 +2156,7 @@ func (m *ResponseDeliverTx) Reset() { *m = ResponseDeliverTx{} } func (m *ResponseDeliverTx) String() string { return proto.CompactTextString(m) } func (*ResponseDeliverTx) ProtoMessage() {} func (*ResponseDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{22} + return fileDescriptor_types_7e896a7c04915591, []int{22} } func (m *ResponseDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2254,7 +2254,7 @@ func (m *ResponseEndBlock) Reset() { *m = ResponseEndBlock{} } func (m *ResponseEndBlock) String() string { return proto.CompactTextString(m) } func (*ResponseEndBlock) ProtoMessage() {} func (*ResponseEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{23} + return fileDescriptor_types_7e896a7c04915591, []int{23} } func (m *ResponseEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2316,7 +2316,7 @@ func (m *ResponseCommit) Reset() { *m = ResponseCommit{} } func (m *ResponseCommit) String() string { return proto.CompactTextString(m) } func (*ResponseCommit) ProtoMessage() {} func (*ResponseCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{24} + return fileDescriptor_types_7e896a7c04915591, []int{24} } func (m *ResponseCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2367,7 +2367,7 @@ func (m *ConsensusParams) Reset() { *m = ConsensusParams{} } func (m *ConsensusParams) String() string { return proto.CompactTextString(m) } func (*ConsensusParams) ProtoMessage() {} func (*ConsensusParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{25} + return fileDescriptor_types_7e896a7c04915591, []int{25} } func (m *ConsensusParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2417,7 +2417,7 @@ func (m *ConsensusParams) GetValidator() *ValidatorParams { return nil } -// BlockParams contains limits on the block size and timestamp. +// BlockParams contains limits on the block size. type BlockParams struct { // Note: must be greater than 0 MaxBytes int64 `protobuf:"varint,1,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"` @@ -2432,7 +2432,7 @@ func (m *BlockParams) Reset() { *m = BlockParams{} } func (m *BlockParams) String() string { return proto.CompactTextString(m) } func (*BlockParams) ProtoMessage() {} func (*BlockParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{26} + return fileDescriptor_types_7e896a7c04915591, []int{26} } func (m *BlockParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2488,7 +2488,7 @@ func (m *EvidenceParams) Reset() { *m = EvidenceParams{} } func (m *EvidenceParams) String() string { return proto.CompactTextString(m) } func (*EvidenceParams) ProtoMessage() {} func (*EvidenceParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{27} + return fileDescriptor_types_7e896a7c04915591, []int{27} } func (m *EvidenceParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2536,7 +2536,7 @@ func (m *ValidatorParams) Reset() { *m = ValidatorParams{} } func (m *ValidatorParams) String() string { return proto.CompactTextString(m) } func (*ValidatorParams) ProtoMessage() {} func (*ValidatorParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{28} + return fileDescriptor_types_7e896a7c04915591, []int{28} } func (m *ValidatorParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2584,7 +2584,7 @@ func (m *LastCommitInfo) Reset() { *m = LastCommitInfo{} } func (m *LastCommitInfo) String() string { return proto.CompactTextString(m) } func (*LastCommitInfo) ProtoMessage() {} func (*LastCommitInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{29} + return fileDescriptor_types_7e896a7c04915591, []int{29} } func (m *LastCommitInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2658,7 +2658,7 @@ func (m *Header) Reset() { *m = Header{} } func (m *Header) String() string { return proto.CompactTextString(m) } func (*Header) ProtoMessage() {} func (*Header) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{30} + return fileDescriptor_types_7e896a7c04915591, []int{30} } func (m *Header) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2811,7 +2811,7 @@ func (m *Version) Reset() { *m = Version{} } func (m *Version) String() string { return proto.CompactTextString(m) } func (*Version) ProtoMessage() {} func (*Version) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{31} + return fileDescriptor_types_7e896a7c04915591, []int{31} } func (m *Version) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2866,7 +2866,7 @@ func (m *BlockID) Reset() { *m = BlockID{} } func (m *BlockID) String() string { return proto.CompactTextString(m) } func (*BlockID) ProtoMessage() {} func (*BlockID) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{32} + return fileDescriptor_types_7e896a7c04915591, []int{32} } func (m *BlockID) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2921,7 +2921,7 @@ func (m *PartSetHeader) Reset() { *m = PartSetHeader{} } func (m *PartSetHeader) String() string { return proto.CompactTextString(m) } func (*PartSetHeader) ProtoMessage() {} func (*PartSetHeader) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{33} + return fileDescriptor_types_7e896a7c04915591, []int{33} } func (m *PartSetHeader) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2978,7 +2978,7 @@ func (m *Validator) Reset() { *m = Validator{} } func (m *Validator) String() string { return proto.CompactTextString(m) } func (*Validator) ProtoMessage() {} func (*Validator) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{34} + return fileDescriptor_types_7e896a7c04915591, []int{34} } func (m *Validator) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3034,7 +3034,7 @@ func (m *ValidatorUpdate) Reset() { *m = ValidatorUpdate{} } func (m *ValidatorUpdate) String() string { return proto.CompactTextString(m) } func (*ValidatorUpdate) ProtoMessage() {} func (*ValidatorUpdate) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{35} + return fileDescriptor_types_7e896a7c04915591, []int{35} } func (m *ValidatorUpdate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3090,7 +3090,7 @@ func (m *VoteInfo) Reset() { *m = VoteInfo{} } func (m *VoteInfo) String() string { return proto.CompactTextString(m) } func (*VoteInfo) ProtoMessage() {} func (*VoteInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{36} + return fileDescriptor_types_7e896a7c04915591, []int{36} } func (m *VoteInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3145,7 +3145,7 @@ func (m *PubKey) Reset() { *m = PubKey{} } func (m *PubKey) String() string { return proto.CompactTextString(m) } func (*PubKey) ProtoMessage() {} func (*PubKey) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{37} + return fileDescriptor_types_7e896a7c04915591, []int{37} } func (m *PubKey) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3203,7 +3203,7 @@ func (m *Evidence) Reset() { *m = Evidence{} } func (m *Evidence) String() string { return proto.CompactTextString(m) } func (*Evidence) ProtoMessage() {} func (*Evidence) Descriptor() ([]byte, []int) { - return fileDescriptor_types_a177e47fab90f91d, []int{38} + return fileDescriptor_types_7e896a7c04915591, []int{38} } func (m *Evidence) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -15357,12 +15357,12 @@ var ( ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") ) -func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_a177e47fab90f91d) } +func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7e896a7c04915591) } func init() { - golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_a177e47fab90f91d) + golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7e896a7c04915591) } -var fileDescriptor_types_a177e47fab90f91d = []byte{ +var fileDescriptor_types_7e896a7c04915591 = []byte{ // 2203 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x58, 0xcf, 0x73, 0x1c, 0x47, 0xf5, 0xd7, 0xec, 0xef, 0x79, 0xab, 0xfd, 0xe1, 0xb6, 0x6c, 0xaf, 0xf7, 0x9b, 0xaf, 0xe4, 0x1a, diff --git a/abci/types/types.proto b/abci/types/types.proto index 8eeecb392..4fdf575c6 100644 --- a/abci/types/types.proto +++ b/abci/types/types.proto @@ -212,7 +212,7 @@ message ConsensusParams { ValidatorParams validator = 3; } -// BlockParams contains limits on the block size and timestamp. +// BlockParams contains limits on the block size. message BlockParams { // Note: must be greater than 0 int64 max_bytes = 1; From 8711af608fff1cfe8d1de1ba8e91d6b0a25a9211 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 3 May 2019 17:21:56 +0400 Subject: [PATCH 019/211] p2p: make persistent prop independent of conn direction (#3593) ## Description Previously only outbound peers can be persistent. Now, even if the peer is inbound, if it's marked as persistent, when/if conn is lost, Tendermint will try to reconnect. This part is actually optional and can be reverted. Plus, seed won't disconnect from inbound peer if it's marked as persistent. Fixes #3362 ## Commits * make persistent prop independent of conn direction Previously only outbound peers can be persistent. Now, even if the peer is inbound, if it's marked as persistent, when/if conn is lost, Tendermint will try to reconnect. Plus, seed won't disconnect from inbound peer if it's marked as persistent. Fixes #3362 * fix TestPEXReactorDialPeer test * add a changelog entry * update changelog * add two tests * reformat code * test UnsafeDialPeers and UnsafeDialSeeds * add TestSwitchDialPeersAsync * spec: update p2p/config spec * fixes after Ismail's review * Apply suggestions from code review Co-Authored-By: melekes * fix merge conflict * remove sleep from TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode We don't need it actually. --- CHANGELOG_PENDING.md | 5 ++ docs/spec/p2p/config.md | 12 ++-- node/node.go | 13 ++-- p2p/pex/pex_reactor.go | 5 +- p2p/pex/pex_reactor_test.go | 42 ++++++++++++- p2p/switch.go | 121 ++++++++++++++++++++++++++---------- p2p/switch_test.go | 111 +++++++++++++++++++++------------ p2p/transport.go | 28 +++++++-- rpc/core/net.go | 14 ++--- rpc/core/net_test.go | 73 ++++++++++++++++++++++ rpc/core/pipe.go | 3 +- 11 files changed, 323 insertions(+), 104 deletions(-) create mode 100644 rpc/core/net_test.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8f162e072..635d0dd1a 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -23,11 +23,16 @@ - [cli] \#3585 Add option to not clear address book with unsafe reset (@climber73) - [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `-config=` option to `testnet` cmd (@gregdhill) - [cs/replay] \#3460 check appHash for each block +- [rpc] \#3362 `/dial_seeds` & `/dial_peers` return errors if addresses are incorrect (except when IP lookup fails) +- [node] \#3362 returns an error if `persistent_peers` list is invalid (except when IP lookup fails) - [p2p] \#3531 Terminate session on nonce wrapping (@climber73) ### BUG FIXES: - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode to 16 (as a result, the node will stop retrying after a 35 hours time window) - [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests +- [p2p] \#3362 make persistent prop independent of conn direction + * `Switch#DialPeersAsync` now only takes a list of peers + * `Switch#DialPeerWithAddress` now only takes an address - [consensus] \#3067 getBeginBlockValidatorInfo loads validators from stateDB instead of state (@james-ray) - [pex] \#3603 Dial seeds when addrbook needs more addresses (@defunctzombie) diff --git a/docs/spec/p2p/config.md b/docs/spec/p2p/config.md index b31a36736..7ff2b5e8d 100644 --- a/docs/spec/p2p/config.md +++ b/docs/spec/p2p/config.md @@ -12,14 +12,14 @@ and upon incoming connection shares some peers and disconnects. ## Seeds -`--p2p.seeds “1.2.3.4:26656,2.3.4.5:4444”` +`--p2p.seeds “id100000000000000000000000000000000@1.2.3.4:26656,id200000000000000000000000000000000@2.3.4.5:4444”` Dials these seeds when we need more peers. They should return a list of peers and then disconnect. If we already have enough peers in the address book, we may never need to dial them. ## Persistent Peers -`--p2p.persistent_peers “1.2.3.4:26656,2.3.4.5:26656”` +`--p2p.persistent_peers “id100000000000000000000000000000000@1.2.3.4:26656,id200000000000000000000000000000000@2.3.4.5:26656”` Dial these peers and auto-redial them if the connection fails. These are intended to be trusted persistent peers that can help @@ -30,9 +30,9 @@ backoff and will give up after a day of trying to connect. the user will be warned that seeds may auto-close connections and that the node may not be able to keep the connection persistent. -## Private Persistent Peers +## Private Peers -`--p2p.private_persistent_peers “1.2.3.4:26656,2.3.4.5:26656”` +`--p2p.private_peer_ids “id100000000000000000000000000000000,id200000000000000000000000000000000”` -These are persistent peers that we do not add to the address book or -gossip to other peers. They stay private to us. +These are IDs of the peers that we do not add to the address book or gossip to +other peers. They stay private to us. diff --git a/node/node.go b/node/node.go index 36bdb9d57..5af080ad1 100644 --- a/node/node.go +++ b/node/node.go @@ -579,6 +579,11 @@ func NewNode(config *cfg.Config, consensusReactor, evidenceReactor, nodeInfo, nodeKey, p2pLogger, ) + err = sw.AddPersistentPeers(splitAndTrimEmpty(config.P2P.PersistentPeers, ",", " ")) + if err != nil { + return nil, errors.Wrap(err, "could not add peers from persistent_peers field") + } + addrBook := createAddrBookAndSetOnSwitch(config, sw, p2pLogger) // Optionally, start the pex reactor @@ -675,12 +680,8 @@ func (n *Node) OnStart() error { } // Always connect to persistent peers - if n.config.P2P.PersistentPeers != "" { - err = n.sw.DialPeersAsync(n.addrBook, splitAndTrimEmpty(n.config.P2P.PersistentPeers, ",", " "), true) - if err != nil { - return err - } - } + // parsing errors are handled above by AddPersistentPeers + _ = n.sw.DialPeersAsync(splitAndTrimEmpty(n.config.P2P.PersistentPeers, ",", " ")) return nil } diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index f24f44dd5..957dbf802 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -531,8 +531,7 @@ func (r *PEXReactor) dialPeer(addr *p2p.NetAddress) error { } } - err := r.Switch.DialPeerWithAddress(addr, false) - + err := r.Switch.DialPeerWithAddress(addr) if err != nil { if _, ok := err.(p2p.ErrCurrentlyDialingOrExistingAddress); ok { return err @@ -584,7 +583,7 @@ func (r *PEXReactor) dialSeeds() { for _, i := range perm { // dial a random seed seedAddr := r.seedAddrs[i] - err := r.Switch.DialPeerWithAddress(seedAddr, false) + err := r.Switch.DialPeerWithAddress(seedAddr) if err == nil { return } diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 6572a5f61..8c52a25ee 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -291,7 +291,8 @@ func TestPEXReactorSeedMode(t *testing.T) { require.Nil(t, err) defer os.RemoveAll(dir) // nolint: errcheck - pexR, book := createReactor(&PEXReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 10 * time.Millisecond}) + pexRConfig := &PEXReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 10 * time.Millisecond} + pexR, book := createReactor(pexRConfig) defer teardownReactor(book) sw := createSwitchAndAddReactors(pexR) @@ -315,13 +316,48 @@ func TestPEXReactorSeedMode(t *testing.T) { pexR.attemptDisconnects() assert.Equal(t, 1, sw.Peers().Size()) - time.Sleep(100 * time.Millisecond) + // sleep for SeedDisconnectWaitPeriod + time.Sleep(pexRConfig.SeedDisconnectWaitPeriod + 1*time.Millisecond) // 3. attemptDisconnects should disconnect after wait period pexR.attemptDisconnects() assert.Equal(t, 0, sw.Peers().Size()) } +func TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode(t *testing.T) { + // directory to store address books + dir, err := ioutil.TempDir("", "pex_reactor") + require.Nil(t, err) + defer os.RemoveAll(dir) // nolint: errcheck + + pexR, book := createReactor(&PEXReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 1 * time.Millisecond}) + defer teardownReactor(book) + + sw := createSwitchAndAddReactors(pexR) + sw.SetAddrBook(book) + err = sw.Start() + require.NoError(t, err) + defer sw.Stop() + + assert.Zero(t, sw.Peers().Size()) + + peerSwitch := testCreateDefaultPeer(dir, 1) + require.NoError(t, peerSwitch.Start()) + defer peerSwitch.Stop() + + err = sw.AddPersistentPeers([]string{peerSwitch.NetAddress().String()}) + require.NoError(t, err) + + // 1. Test crawlPeers dials the peer + pexR.crawlPeers([]*p2p.NetAddress{peerSwitch.NetAddress()}) + assert.Equal(t, 1, sw.Peers().Size()) + assert.True(t, sw.Peers().Has(peerSwitch.NodeInfo().ID())) + + // 2. attemptDisconnects should not disconnect because the peer is persistent + pexR.attemptDisconnects() + assert.Equal(t, 1, sw.Peers().Size()) +} + func TestPEXReactorDialsPeerUpToMaxAttemptsInSeedMode(t *testing.T) { // directory to store address books dir, err := ioutil.TempDir("", "pex_reactor") @@ -398,7 +434,7 @@ func TestPEXReactorSeedModeFlushStop(t *testing.T) { reactor := switches[0].Reactors()["pex"].(*PEXReactor) peerID := switches[1].NodeInfo().ID() - err = switches[1].DialPeerWithAddress(switches[0].NetAddress(), false) + err = switches[1].DialPeerWithAddress(switches[0].NetAddress()) assert.NoError(t, err) // sleep up to a second while waiting for the peer to send us a message. diff --git a/p2p/switch.go b/p2p/switch.go index a566e6029..ccb7119d2 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -77,6 +77,8 @@ type Switch struct { nodeInfo NodeInfo // our node info nodeKey *NodeKey // our node privkey addrBook AddrBook + // peers addresses with whom we'll maintain constant connection + persistentPeersAddrs []*NetAddress transport Transport @@ -104,16 +106,17 @@ func NewSwitch( options ...SwitchOption, ) *Switch { sw := &Switch{ - config: cfg, - reactors: make(map[string]Reactor), - chDescs: make([]*conn.ChannelDescriptor, 0), - reactorsByCh: make(map[byte]Reactor), - peers: NewPeerSet(), - dialing: cmn.NewCMap(), - reconnecting: cmn.NewCMap(), - metrics: NopMetrics(), - transport: transport, - filterTimeout: defaultFilterTimeout, + config: cfg, + reactors: make(map[string]Reactor), + chDescs: make([]*conn.ChannelDescriptor, 0), + reactorsByCh: make(map[byte]Reactor), + peers: NewPeerSet(), + dialing: cmn.NewCMap(), + reconnecting: cmn.NewCMap(), + metrics: NopMetrics(), + transport: transport, + filterTimeout: defaultFilterTimeout, + persistentPeersAddrs: make([]*NetAddress, 0), } // Ensure we have a completely undeterministic PRNG. @@ -297,7 +300,19 @@ func (sw *Switch) StopPeerForError(peer Peer, reason interface{}) { sw.stopAndRemovePeer(peer, reason) if peer.IsPersistent() { - go sw.reconnectToPeer(peer.SocketAddr()) + var addr *NetAddress + if peer.IsOutbound() { // socket address for outbound peers + addr = peer.SocketAddr() + } else { // self-reported address for inbound peers + var err error + addr, err = peer.NodeInfo().NetAddress() + if err != nil { + sw.Logger.Error("Wanted to reconnect to inbound peer, but self-reported address is wrong", + "peer", peer, "addr", addr, "err", err) + return + } + } + go sw.reconnectToPeer(addr) } } @@ -341,7 +356,7 @@ func (sw *Switch) reconnectToPeer(addr *NetAddress) { return } - err := sw.DialPeerWithAddress(addr, true) + err := sw.DialPeerWithAddress(addr) if err == nil { return // success } else if _, ok := err.(ErrCurrentlyDialingOrExistingAddress); ok { @@ -365,7 +380,7 @@ func (sw *Switch) reconnectToPeer(addr *NetAddress) { sleepIntervalSeconds := math.Pow(reconnectBackOffBaseSeconds, float64(i)) sw.randomSleep(time.Duration(sleepIntervalSeconds) * time.Second) - err := sw.DialPeerWithAddress(addr, true) + err := sw.DialPeerWithAddress(addr) if err == nil { return // success } else if _, ok := err.(ErrCurrentlyDialingOrExistingAddress); ok { @@ -401,28 +416,41 @@ func isPrivateAddr(err error) bool { return ok && te.PrivateAddr() } -// DialPeersAsync dials a list of peers asynchronously in random order (optionally, making them persistent). +// DialPeersAsync dials a list of peers asynchronously in random order. // Used to dial peers from config on startup or from unsafe-RPC (trusted sources). -// TODO: remove addrBook arg since it's now set on the switch -func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent bool) error { +// It ignores ErrNetAddressLookup. However, if there are other errors, first +// encounter is returned. +// Nop if there are no peers. +func (sw *Switch) DialPeersAsync(peers []string) error { netAddrs, errs := NewNetAddressStrings(peers) - // only log errors, dial correct addresses + // report all the errors for _, err := range errs { sw.Logger.Error("Error in peer's address", "err", err) } + // return first non-ErrNetAddressLookup error + for _, err := range errs { + if _, ok := err.(ErrNetAddressLookup); ok { + continue + } + return err + } + sw.dialPeersAsync(netAddrs) + return nil +} +func (sw *Switch) dialPeersAsync(netAddrs []*NetAddress) { ourAddr := sw.NetAddress() // TODO: this code feels like it's in the wrong place. // The integration tests depend on the addrBook being saved // right away but maybe we can change that. Recall that // the addrBook is only written to disk every 2min - if addrBook != nil { + if sw.addrBook != nil { // add peers to `addrBook` for _, netAddr := range netAddrs { // do not add our address or ID if !netAddr.Same(ourAddr) { - if err := addrBook.AddAddress(netAddr, ourAddr); err != nil { + if err := sw.addrBook.AddAddress(netAddr, ourAddr); err != nil { if isPrivateAddr(err) { sw.Logger.Debug("Won't add peer's address to addrbook", "err", err) } else { @@ -433,7 +461,7 @@ func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent b } // Persist some peers to disk right away. // NOTE: integration tests depend on this - addrBook.Save() + sw.addrBook.Save() } // permute the list, dial them in random order. @@ -450,7 +478,7 @@ func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent b sw.randomSleep(0) - err := sw.DialPeerWithAddress(addr, persistent) + err := sw.DialPeerWithAddress(addr) if err != nil { switch err.(type) { case ErrSwitchConnectToSelf, ErrSwitchDuplicatePeerID, ErrCurrentlyDialingOrExistingAddress: @@ -461,16 +489,13 @@ func (sw *Switch) DialPeersAsync(addrBook AddrBook, peers []string, persistent b } }(i) } - return nil } // DialPeerWithAddress dials the given peer and runs sw.addPeer if it connects // and authenticates successfully. -// If `persistent == true`, the switch will always try to reconnect to this -// peer if the connection ever fails. // If we're currently dialing this address or it belongs to an existing peer, // ErrCurrentlyDialingOrExistingAddress is returned. -func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) error { +func (sw *Switch) DialPeerWithAddress(addr *NetAddress) error { if sw.IsDialingOrExistingAddress(addr) { return ErrCurrentlyDialingOrExistingAddress{addr.String()} } @@ -478,7 +503,7 @@ func (sw *Switch) DialPeerWithAddress(addr *NetAddress, persistent bool) error { sw.dialing.Set(string(addr.ID), addr) defer sw.dialing.Delete(string(addr.ID)) - return sw.addOutboundPeerWithConfig(addr, sw.config, persistent) + return sw.addOutboundPeerWithConfig(addr, sw.config) } // sleep for interval plus some random amount of ms on [0, dialRandomizerIntervalMilliseconds] @@ -495,6 +520,38 @@ func (sw *Switch) IsDialingOrExistingAddress(addr *NetAddress) bool { (!sw.config.AllowDuplicateIP && sw.peers.HasIP(addr.IP)) } +// AddPersistentPeers allows you to set persistent peers. It ignores +// ErrNetAddressLookup. However, if there are other errors, first encounter is +// returned. +func (sw *Switch) AddPersistentPeers(addrs []string) error { + sw.Logger.Info("Adding persistent peers", "addrs", addrs) + netAddrs, errs := NewNetAddressStrings(addrs) + // report all the errors + for _, err := range errs { + sw.Logger.Error("Error in peer's address", "err", err) + } + // return first non-ErrNetAddressLookup error + for _, err := range errs { + if _, ok := err.(ErrNetAddressLookup); ok { + continue + } + return err + } + sw.persistentPeersAddrs = netAddrs + return nil +} + +func (sw *Switch) isPeerPersistentFn() func(*NetAddress) bool { + return func(na *NetAddress) bool { + for _, pa := range sw.persistentPeersAddrs { + if pa.Equals(na) { + return true + } + } + return false + } +} + func (sw *Switch) acceptRoutine() { for { p, err := sw.transport.Accept(peerConfig{ @@ -502,6 +559,7 @@ func (sw *Switch) acceptRoutine() { onPeerError: sw.StopPeerForError, reactorsByCh: sw.reactorsByCh, metrics: sw.metrics, + isPersistent: sw.isPeerPersistentFn(), }) if err != nil { switch err := err.(type) { @@ -581,13 +639,12 @@ func (sw *Switch) acceptRoutine() { // dial the peer; make secret connection; authenticate against the dialed ID; // add the peer. -// if dialing fails, start the reconnect loop. If handhsake fails, its over. -// If peer is started succesffuly, reconnectLoop will start when -// StopPeerForError is called +// if dialing fails, start the reconnect loop. If handshake fails, it's over. +// If peer is started successfully, reconnectLoop will start when +// StopPeerForError is called. func (sw *Switch) addOutboundPeerWithConfig( addr *NetAddress, cfg *config.P2PConfig, - persistent bool, ) error { sw.Logger.Info("Dialing peer", "address", addr) @@ -600,7 +657,7 @@ func (sw *Switch) addOutboundPeerWithConfig( p, err := sw.transport.Dial(*addr, peerConfig{ chDescs: sw.chDescs, onPeerError: sw.StopPeerForError, - persistent: persistent, + isPersistent: sw.isPeerPersistentFn(), reactorsByCh: sw.reactorsByCh, metrics: sw.metrics, }) @@ -619,7 +676,7 @@ func (sw *Switch) addOutboundPeerWithConfig( // retry persistent peers after // any dial error besides IsSelf() - if persistent { + if sw.isPeerPersistentFn()(addr) { go sw.reconnectToPeer(addr) } diff --git a/p2p/switch_test.go b/p2p/switch_test.go index bf105e0fa..6c7538b51 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -167,7 +167,7 @@ func TestSwitchFiltersOutItself(t *testing.T) { rp.Start() // addr should be rejected in addPeer based on the same ID - err := s1.DialPeerWithAddress(rp.Addr(), false) + err := s1.DialPeerWithAddress(rp.Addr()) if assert.Error(t, err) { if err, ok := err.(ErrRejected); ok { if !err.IsSelf() { @@ -212,6 +212,7 @@ func TestSwitchPeerFilter(t *testing.T) { p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ chDescs: sw.chDescs, onPeerError: sw.StopPeerForError, + isPersistent: sw.isPeerPersistentFn(), reactorsByCh: sw.reactorsByCh, }) if err != nil { @@ -256,6 +257,7 @@ func TestSwitchPeerFilterTimeout(t *testing.T) { p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ chDescs: sw.chDescs, onPeerError: sw.StopPeerForError, + isPersistent: sw.isPeerPersistentFn(), reactorsByCh: sw.reactorsByCh, }) if err != nil { @@ -281,6 +283,7 @@ func TestSwitchPeerFilterDuplicate(t *testing.T) { p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ chDescs: sw.chDescs, onPeerError: sw.StopPeerForError, + isPersistent: sw.isPeerPersistentFn(), reactorsByCh: sw.reactorsByCh, }) if err != nil { @@ -326,6 +329,7 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ chDescs: sw.chDescs, onPeerError: sw.StopPeerForError, + isPersistent: sw.isPeerPersistentFn(), reactorsByCh: sw.reactorsByCh, }) require.Nil(err) @@ -390,49 +394,33 @@ func TestSwitchStopPeerForError(t *testing.T) { assert.EqualValues(t, 0, peersMetricValue()) } -func TestSwitchReconnectsToPersistentPeer(t *testing.T) { - assert, require := assert.New(t), require.New(t) - +func TestSwitchReconnectsToOutboundPersistentPeer(t *testing.T) { sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) err := sw.Start() - if err != nil { - t.Error(err) - } + require.NoError(t, err) defer sw.Stop() - // simulate remote peer + // 1. simulate failure by closing connection rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} rp.Start() defer rp.Stop() - p, err := sw.transport.Dial(*rp.Addr(), peerConfig{ - chDescs: sw.chDescs, - onPeerError: sw.StopPeerForError, - persistent: true, - reactorsByCh: sw.reactorsByCh, - }) - require.Nil(err) - - require.Nil(sw.addPeer(p)) + err = sw.AddPersistentPeers([]string{rp.Addr().String()}) + require.NoError(t, err) - require.NotNil(sw.Peers().Get(rp.ID())) + err = sw.DialPeerWithAddress(rp.Addr()) + require.Nil(t, err) + time.Sleep(50 * time.Millisecond) + require.NotNil(t, sw.Peers().Get(rp.ID())) - // simulate failure by closing connection + p := sw.Peers().List()[0] p.(*peer).CloseConn() - // TODO: remove sleep, detect the disconnection, wait for reconnect - npeers := sw.Peers().Size() - for i := 0; i < 20; i++ { - time.Sleep(250 * time.Millisecond) - npeers = sw.Peers().Size() - if npeers > 0 { - break - } - } - assert.NotZero(npeers) - assert.False(p.IsRunning()) + waitUntilSwitchHasAtLeastNPeers(sw, 1) + assert.False(t, p.IsRunning()) // old peer instance + assert.Equal(t, 1, sw.Peers().Size()) // new peer instance - // simulate another remote peer + // 2. simulate first time dial failure rp = &remotePeer{ PrivKey: ed25519.GenPrivKey(), Config: cfg, @@ -443,23 +431,68 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { rp.Start() defer rp.Stop() - // simulate first time dial failure conf := config.DefaultP2PConfig() conf.TestDialFail = true - err = sw.addOutboundPeerWithConfig(rp.Addr(), conf, true) - require.NotNil(err) - + err = sw.addOutboundPeerWithConfig(rp.Addr(), conf) + require.NotNil(t, err) // DialPeerWithAddres - sw.peerConfig resets the dialer + waitUntilSwitchHasAtLeastNPeers(sw, 2) + assert.Equal(t, 2, sw.Peers().Size()) +} + +func TestSwitchReconnectsToInboundPersistentPeer(t *testing.T) { + sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) + err := sw.Start() + require.NoError(t, err) + defer sw.Stop() + + // 1. simulate failure by closing the connection + rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} + rp.Start() + defer rp.Stop() + + err = sw.AddPersistentPeers([]string{rp.Addr().String()}) + require.NoError(t, err) + + conn, err := rp.Dial(sw.NetAddress()) + require.NoError(t, err) + time.Sleep(50 * time.Millisecond) + require.NotNil(t, sw.Peers().Get(rp.ID())) + + conn.Close() + + waitUntilSwitchHasAtLeastNPeers(sw, 1) + assert.Equal(t, 1, sw.Peers().Size()) +} + +func TestSwitchDialPeersAsync(t *testing.T) { + if testing.Short() { + return + } + + sw := MakeSwitch(cfg, 1, "testing", "123.123.123", initSwitchFunc) + err := sw.Start() + require.NoError(t, err) + defer sw.Stop() + + rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} + rp.Start() + defer rp.Stop() + + err = sw.DialPeersAsync([]string{rp.Addr().String()}) + require.NoError(t, err) + time.Sleep(dialRandomizerIntervalMilliseconds * time.Millisecond) + require.NotNil(t, sw.Peers().Get(rp.ID())) +} - // TODO: same as above +func waitUntilSwitchHasAtLeastNPeers(sw *Switch, n int) { for i := 0; i < 20; i++ { time.Sleep(250 * time.Millisecond) - npeers = sw.Peers().Size() - if npeers > 1 { + has := sw.Peers().Size() + if has >= n { break } } - assert.EqualValues(2, npeers) } func TestSwitchFullConnectivity(t *testing.T) { diff --git a/p2p/transport.go b/p2p/transport.go index ebf77c9f4..8d6ea236e 100644 --- a/p2p/transport.go +++ b/p2p/transport.go @@ -37,11 +37,15 @@ type accept struct { // events. // TODO(xla): Refactor out with more static Reactor setup and PeerBehaviour. type peerConfig struct { - chDescs []*conn.ChannelDescriptor - onPeerError func(Peer, interface{}) - outbound, persistent bool - reactorsByCh map[byte]Reactor - metrics *Metrics + chDescs []*conn.ChannelDescriptor + onPeerError func(Peer, interface{}) + outbound bool + // isPersistent allows you to set a function, which, given socket address + // (for outbound peers) OR self-reported address (for inbound peers), tells + // if the peer is persistent or not. + isPersistent func(*NetAddress) bool + reactorsByCh map[byte]Reactor + metrics *Metrics } // Transport emits and connects to Peers. The implementation of Peer is left to @@ -446,9 +450,21 @@ func (mt *MultiplexTransport) wrapPeer( socketAddr *NetAddress, ) Peer { + persistent := false + if cfg.isPersistent != nil { + if cfg.outbound { + persistent = cfg.isPersistent(socketAddr) + } else { + selfReportedAddr, err := ni.NetAddress() + if err == nil { + persistent = cfg.isPersistent(selfReportedAddr) + } + } + } + peerConn := newPeerConn( cfg.outbound, - cfg.persistent, + persistent, c, socketAddr, ) diff --git a/rpc/core/net.go b/rpc/core/net.go index 23bc40e88..48bf576a8 100644 --- a/rpc/core/net.go +++ b/rpc/core/net.go @@ -184,10 +184,8 @@ func UnsafeDialSeeds(ctx *rpctypes.Context, seeds []string) (*ctypes.ResultDialS if len(seeds) == 0 { return &ctypes.ResultDialSeeds{}, errors.New("No seeds provided") } - // starts go routines to dial each peer after random delays - logger.Info("DialSeeds", "addrBook", addrBook, "seeds", seeds) - err := p2pPeers.DialPeersAsync(addrBook, seeds, false) - if err != nil { + logger.Info("DialSeeds", "seeds", seeds) + if err := p2pPeers.DialPeersAsync(seeds); err != nil { return &ctypes.ResultDialSeeds{}, err } return &ctypes.ResultDialSeeds{Log: "Dialing seeds in progress. See /net_info for details"}, nil @@ -197,12 +195,12 @@ func UnsafeDialPeers(ctx *rpctypes.Context, peers []string, persistent bool) (*c if len(peers) == 0 { return &ctypes.ResultDialPeers{}, errors.New("No peers provided") } - // starts go routines to dial each peer after random delays - logger.Info("DialPeers", "addrBook", addrBook, "peers", peers, "persistent", persistent) - err := p2pPeers.DialPeersAsync(addrBook, peers, persistent) - if err != nil { + logger.Info("DialPeers", "peers", peers, "persistent", persistent) + if err := p2pPeers.AddPersistentPeers(peers); err != nil { return &ctypes.ResultDialPeers{}, err } + // parsing errors are handled above by AddPersistentPeers + _ = p2pPeers.DialPeersAsync(peers) return &ctypes.ResultDialPeers{Log: "Dialing peers in progress. See /net_info for details"}, nil } diff --git a/rpc/core/net_test.go b/rpc/core/net_test.go new file mode 100644 index 000000000..651e1f69d --- /dev/null +++ b/rpc/core/net_test.go @@ -0,0 +1,73 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cfg "github.com/tendermint/tendermint/config" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" +) + +func TestUnsafeDialSeeds(t *testing.T) { + sw := p2p.MakeSwitch(cfg.DefaultP2PConfig(), 1, "testing", "123.123.123", + func(n int, sw *p2p.Switch) *p2p.Switch { return sw }) + err := sw.Start() + require.NoError(t, err) + defer sw.Stop() + + logger = log.TestingLogger() + p2pPeers = sw + + testCases := []struct { + seeds []string + isErr bool + }{ + {[]string{}, true}, + {[]string{"d51fb70907db1c6c2d5237e78379b25cf1a37ab4@127.0.0.1:41198"}, false}, + {[]string{"127.0.0.1:41198"}, true}, + } + + for _, tc := range testCases { + res, err := UnsafeDialSeeds(&rpctypes.Context{}, tc.seeds) + if tc.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, res) + } + } +} + +func TestUnsafeDialPeers(t *testing.T) { + sw := p2p.MakeSwitch(cfg.DefaultP2PConfig(), 1, "testing", "123.123.123", + func(n int, sw *p2p.Switch) *p2p.Switch { return sw }) + err := sw.Start() + require.NoError(t, err) + defer sw.Stop() + + logger = log.TestingLogger() + p2pPeers = sw + + testCases := []struct { + peers []string + isErr bool + }{ + {[]string{}, true}, + {[]string{"d51fb70907db1c6c2d5237e78379b25cf1a37ab4@127.0.0.1:41198"}, false}, + {[]string{"127.0.0.1:41198"}, true}, + } + + for _, tc := range testCases { + res, err := UnsafeDialPeers(&rpctypes.Context{}, tc.peers, false) + if tc.isErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotNil(t, res) + } + } +} diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index ad8afdefc..cefb0e371 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -44,7 +44,8 @@ type transport interface { } type peers interface { - DialPeersAsync(p2p.AddrBook, []string, bool) error + AddPersistentPeers([]string) error + DialPeersAsync([]string) error NumPeers() (outbound, inbound, dialig int) Peers() p2p.IPeerSet } From 5051a1f7bcda164835592baccee3caf0d02984ec Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 4 May 2019 10:41:31 +0400 Subject: [PATCH 020/211] mempool: move interface into mempool package (#3524) ## Description Refs #2659 Breaking changes in the mempool package: [mempool] #2659 Mempool now an interface old Mempool renamed to CListMempool NewMempool renamed to NewCListMempool Option renamed to CListOption MempoolReactor renamed to Reactor NewMempoolReactor renamed to NewReactor unexpose TxID method TxInfo.PeerID renamed to SenderID unexpose MempoolReactor.Mempool Breaking changes in the state package: [state] #2659 Mempool interface moved to mempool package MockMempool moved to top-level mock package and renamed to Mempool Non Breaking changes in the node package: [node] #2659 Add Mempool method, which allows you to access mempool ## Commits * move Mempool interface into mempool package Refs #2659 Breaking changes in the mempool package: - Mempool now an interface - old Mempool renamed to CListMempool Breaking changes to state package: - MockMempool moved to mempool/mock package and renamed to Mempool - Mempool interface moved to mempool package * assert CListMempool impl Mempool * gofmt code * rename MempoolReactor to Reactor - combine everything into one interface - rename TxInfo.PeerID to TxInfo.SenderID - unexpose MempoolReactor.Mempool * move mempool mock into top-level mock package * add a fixme TxsFront should not be a part of the Mempool interface because it leaks implementation details. Instead, we need to come up with general interface for querying the mempool so the MempoolReactor can fetch and broadcast txs to peers. * change node#Mempool to return interface * save commit = new reactor arch * Revert "save commit = new reactor arch" This reverts commit 1bfceacd9d65a720574683a7f22771e69af9af4d. * require CListMempool in mempool.Reactor * add two changelog entries * fixes after my own review * quote interfaces, structs and functions * fixes after Ismail's review * make node's mempool an interface * make InitWAL/CloseWAL methods a part of Mempool interface * fix merge conflicts * make node's mempool an interface --- CHANGELOG_PENDING.md | 12 + blockchain/reactor_test.go | 3 +- consensus/common_test.go | 2 +- consensus/mempool_test.go | 5 +- consensus/reactor_test.go | 2 +- consensus/replay.go | 4 +- consensus/replay_file.go | 3 +- consensus/replay_test.go | 3 +- consensus/wal_generator.go | 4 +- mempool/clist_mempool.go | 714 +++++++++++++++ ...{mempool_test.go => clist_mempool_test.go} | 23 +- mempool/doc.go | 24 + mempool/errors.go | 46 + mempool/mempool.go | 862 ++---------------- mempool/reactor.go | 44 +- mempool/reactor_test.go | 47 +- mock/mempool.go | 45 + node/node.go | 39 +- node/node_test.go | 2 +- rpc/client/rpc_test.go | 9 +- rpc/core/pipe.go | 4 +- state/execution.go | 5 +- state/execution_test.go | 10 +- state/services.go | 55 +- 24 files changed, 1022 insertions(+), 945 deletions(-) create mode 100644 mempool/clist_mempool.go rename mempool/{mempool_test.go => clist_mempool_test.go} (96%) create mode 100644 mempool/doc.go create mode 100644 mempool/errors.go create mode 100644 mock/mempool.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 635d0dd1a..3b6dffb3a 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -9,6 +9,17 @@ * Apps * Go API +- [mempool] \#2659 `Mempool` now an interface + * old `Mempool` implementation renamed to `CListMempool` + * `NewMempool` renamed to `NewCListMempool` + * `Option` renamed to `CListOption` + * unexpose `MempoolReactor.Mempool` + * `MempoolReactor` renamed to `Reactor` + * `NewMempoolReactor` renamed to `NewReactor` + * unexpose `TxID` method + * `TxInfo.PeerID` renamed to `SenderID` +- [state] \#2659 `Mempool` interface moved to mempool package + * `MockMempool` moved to top-level mock package and renamed to `Mempool` - [libs/common] Removed `PanicSanity`, `PanicCrisis`, `PanicConsensus` and `PanicQ` - [node] Moved `GenesisDocProvider` and `DefaultGenesisDocProviderFunc` to state package @@ -17,6 +28,7 @@ * P2P Protocol ### FEATURES: +- [node] \#2659 Add `node.Mempool()` method, which allows you to access mempool ### IMPROVEMENTS: - [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 9cd3489c8..52ef9ba64 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -13,6 +13,7 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" @@ -93,7 +94,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals fastSync := true db := dbm.NewMemDB() blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), - sm.MockMempool{}, sm.MockEvidencePool{}) + mock.Mempool{}, sm.MockEvidencePool{}) sm.SaveState(db, state) // let's add some blocks in diff --git a/consensus/common_test.go b/consensus/common_test.go index a0c179ae1..a4ad79c94 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -288,7 +288,7 @@ func newConsensusStateWithConfigAndBlockStore(thisConfig *cfg.Config, state sm.S proxyAppConnCon := abcicli.NewLocalClient(mtx, app) // Make Mempool - mempool := mempl.NewMempool(thisConfig.Mempool, proxyAppConnMem, 0) + mempool := mempl.NewCListMempool(thisConfig.Mempool, proxyAppConnMem, 0) mempool.SetLogger(log.TestingLogger().With("module", "mempool")) if thisConfig.Consensus.WaitForTxs() { mempool.EnableTxsAvailable() diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index 1baa92b89..af15a1fef 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -12,13 +12,14 @@ import ( "github.com/tendermint/tendermint/abci/example/code" abci "github.com/tendermint/tendermint/abci/types" dbm "github.com/tendermint/tendermint/libs/db" + mempl "github.com/tendermint/tendermint/mempool" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) // for testing -func assertMempool(txn txNotifier) sm.Mempool { - return txn.(sm.Mempool) +func assertMempool(txn txNotifier) mempl.Mempool { + return txn.(mempl.Mempool) } func TestMempoolNoProgressUntilTxsAvailable(t *testing.T) { diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index bf58a8eeb..9440b50c3 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -140,7 +140,7 @@ func TestReactorWithEvidence(t *testing.T) { proxyAppConnCon := abcicli.NewLocalClient(mtx, app) // Make Mempool - mempool := mempl.NewMempool(thisConfig.Mempool, proxyAppConnMem, 0) + mempool := mempl.NewCListMempool(thisConfig.Mempool, proxyAppConnMem, 0) mempool.SetLogger(log.TestingLogger().With("module", "mempool")) if thisConfig.Consensus.WaitForTxs() { mempool.EnableTxsAvailable() diff --git a/consensus/replay.go b/consensus/replay.go index cc1344eba..794f870d0 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -16,7 +16,7 @@ import ( dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" - + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" @@ -454,7 +454,7 @@ func (h *Handshaker) replayBlock(state sm.State, height int64, proxyApp proxy.Ap block := h.store.LoadBlock(height) meta := h.store.LoadBlockMeta(height) - blockExec := sm.NewBlockExecutor(h.stateDB, h.logger, proxyApp, sm.MockMempool{}, sm.MockEvidencePool{}) + blockExec := sm.NewBlockExecutor(h.stateDB, h.logger, proxyApp, mock.Mempool{}, sm.MockEvidencePool{}) blockExec.SetEventBus(h.eventBus) var err error diff --git a/consensus/replay_file.go b/consensus/replay_file.go index d45f9c5a4..5bb73484e 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -16,6 +16,7 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" @@ -312,7 +313,7 @@ func newConsensusStateForReplay(config cfg.BaseConfig, csConfig *cfg.ConsensusCo cmn.Exit(fmt.Sprintf("Error on handshake: %v", err)) } - mempool, evpool := sm.MockMempool{}, sm.MockEvidencePool{} + mempool, evpool := mock.Mempool{}, sm.MockEvidencePool{} blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool) consensusState := NewConsensusState(csConfig, state.Copy(), blockExec, diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 6ed361905..724dd056b 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -24,6 +24,7 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" @@ -277,7 +278,7 @@ const ( ) var ( - mempool = sm.MockMempool{} + mempool = mock.Mempool{} evpool = sm.MockEvidencePool{} sim testSim diff --git a/consensus/wal_generator.go b/consensus/wal_generator.go index c007a21cf..2faff27b5 100644 --- a/consensus/wal_generator.go +++ b/consensus/wal_generator.go @@ -10,12 +10,14 @@ import ( "time" "github.com/pkg/errors" + "github.com/tendermint/tendermint/abci/example/kvstore" bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" @@ -67,7 +69,7 @@ func WALGenerateNBlocks(t *testing.T, wr io.Writer, numBlocks int) (err error) { return errors.Wrap(err, "failed to start event bus") } defer eventBus.Stop() - mempool := sm.MockMempool{} + mempool := mock.Mempool{} evpool := sm.MockEvidencePool{} blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mempool, evpool) consensusState := NewConsensusState(config.Consensus, state.Copy(), blockExec, blockStore, mempool, evpool) diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go new file mode 100644 index 000000000..2a4bacbbb --- /dev/null +++ b/mempool/clist_mempool.go @@ -0,0 +1,714 @@ +package mempool + +import ( + "bytes" + "container/list" + "crypto/sha256" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/pkg/errors" + + abci "github.com/tendermint/tendermint/abci/types" + cfg "github.com/tendermint/tendermint/config" + auto "github.com/tendermint/tendermint/libs/autofile" + "github.com/tendermint/tendermint/libs/clist" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/proxy" + "github.com/tendermint/tendermint/types" +) + +//-------------------------------------------------------------------------------- + +// CListMempool is an ordered in-memory pool for transactions before they are +// proposed in a consensus round. Transaction validity is checked using the +// CheckTx abci message before the transaction is added to the pool. The +// mempool uses a concurrent list structure for storing transactions that can +// be efficiently accessed by multiple concurrent readers. +type CListMempool struct { + config *cfg.MempoolConfig + + proxyMtx sync.Mutex + proxyAppConn proxy.AppConnMempool + txs *clist.CList // concurrent linked-list of good txs + preCheck PreCheckFunc + postCheck PostCheckFunc + + // Track whether we're rechecking txs. + // These are not protected by a mutex and are expected to be mutated + // in serial (ie. by abci responses which are called in serial). + recheckCursor *clist.CElement // next expected response + recheckEnd *clist.CElement // re-checking stops here + + // notify listeners (ie. consensus) when txs are available + notifiedTxsAvailable bool + txsAvailable chan struct{} // fires once for each height, when the mempool is not empty + + // Map for quick access to txs to record sender in CheckTx. + // txsMap: txKey -> CElement + txsMap sync.Map + + // Atomic integers + height int64 // the last block Update()'d to + rechecking int32 // for re-checking filtered txs on Update() + txsBytes int64 // total size of mempool, in bytes + + // Keep a cache of already-seen txs. + // This reduces the pressure on the proxyApp. + cache txCache + + // A log of mempool txs + wal *auto.AutoFile + + logger log.Logger + + metrics *Metrics +} + +var _ Mempool = &CListMempool{} + +// CListMempoolOption sets an optional parameter on the mempool. +type CListMempoolOption func(*CListMempool) + +// NewCListMempool returns a new mempool with the given configuration and connection to an application. +func NewCListMempool( + config *cfg.MempoolConfig, + proxyAppConn proxy.AppConnMempool, + height int64, + options ...CListMempoolOption, +) *CListMempool { + mempool := &CListMempool{ + config: config, + proxyAppConn: proxyAppConn, + txs: clist.New(), + height: height, + rechecking: 0, + recheckCursor: nil, + recheckEnd: nil, + logger: log.NewNopLogger(), + metrics: NopMetrics(), + } + if config.CacheSize > 0 { + mempool.cache = newMapTxCache(config.CacheSize) + } else { + mempool.cache = nopTxCache{} + } + proxyAppConn.SetResponseCallback(mempool.globalCb) + for _, option := range options { + option(mempool) + } + return mempool +} + +// NOTE: not thread safe - should only be called once, on startup +func (mem *CListMempool) EnableTxsAvailable() { + mem.txsAvailable = make(chan struct{}, 1) +} + +// SetLogger sets the Logger. +func (mem *CListMempool) SetLogger(l log.Logger) { + mem.logger = l +} + +// WithPreCheck sets a filter for the mempool to reject a tx if f(tx) returns +// false. This is ran before CheckTx. +func WithPreCheck(f PreCheckFunc) CListMempoolOption { + return func(mem *CListMempool) { mem.preCheck = f } +} + +// WithPostCheck sets a filter for the mempool to reject a tx if f(tx) returns +// false. This is ran after CheckTx. +func WithPostCheck(f PostCheckFunc) CListMempoolOption { + return func(mem *CListMempool) { mem.postCheck = f } +} + +// WithMetrics sets the metrics. +func WithMetrics(metrics *Metrics) CListMempoolOption { + return func(mem *CListMempool) { mem.metrics = metrics } +} + +// *panics* if can't create directory or open file. +// *not thread safe* +func (mem *CListMempool) InitWAL() { + walDir := mem.config.WalDir() + err := cmn.EnsureDir(walDir, 0700) + if err != nil { + panic(errors.Wrap(err, "Error ensuring WAL dir")) + } + af, err := auto.OpenAutoFile(walDir + "/wal") + if err != nil { + panic(errors.Wrap(err, "Error opening WAL file")) + } + mem.wal = af +} + +func (mem *CListMempool) CloseWAL() { + mem.proxyMtx.Lock() + defer mem.proxyMtx.Unlock() + + if err := mem.wal.Close(); err != nil { + mem.logger.Error("Error closing WAL", "err", err) + } + mem.wal = nil +} + +func (mem *CListMempool) Lock() { + mem.proxyMtx.Lock() +} + +func (mem *CListMempool) Unlock() { + mem.proxyMtx.Unlock() +} + +func (mem *CListMempool) Size() int { + return mem.txs.Len() +} + +func (mem *CListMempool) TxsBytes() int64 { + return atomic.LoadInt64(&mem.txsBytes) +} + +func (mem *CListMempool) FlushAppConn() error { + return mem.proxyAppConn.FlushSync() +} + +func (mem *CListMempool) Flush() { + mem.proxyMtx.Lock() + defer mem.proxyMtx.Unlock() + + mem.cache.Reset() + + for e := mem.txs.Front(); e != nil; e = e.Next() { + mem.txs.Remove(e) + e.DetachPrev() + } + + mem.txsMap = sync.Map{} + _ = atomic.SwapInt64(&mem.txsBytes, 0) +} + +// TxsFront returns the first transaction in the ordered list for peer +// goroutines to call .NextWait() on. +// FIXME: leaking implementation details! +func (mem *CListMempool) TxsFront() *clist.CElement { + return mem.txs.Front() +} + +// TxsWaitChan returns a channel to wait on transactions. It will be closed +// once the mempool is not empty (ie. the internal `mem.txs` has at least one +// element) +func (mem *CListMempool) TxsWaitChan() <-chan struct{} { + return mem.txs.WaitChan() +} + +// It blocks if we're waiting on Update() or Reap(). +// cb: A callback from the CheckTx command. +// It gets called from another goroutine. +// CONTRACT: Either cb will get called, or err returned. +func (mem *CListMempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err error) { + return mem.CheckTxWithInfo(tx, cb, TxInfo{SenderID: UnknownPeerID}) +} + +func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), txInfo TxInfo) (err error) { + mem.proxyMtx.Lock() + // use defer to unlock mutex because application (*local client*) might panic + defer mem.proxyMtx.Unlock() + + var ( + memSize = mem.Size() + txsBytes = mem.TxsBytes() + ) + if memSize >= mem.config.Size || + int64(len(tx))+txsBytes > mem.config.MaxTxsBytes { + return ErrMempoolIsFull{ + memSize, mem.config.Size, + txsBytes, mem.config.MaxTxsBytes} + } + + // The size of the corresponding amino-encoded TxMessage + // can't be larger than the maxMsgSize, otherwise we can't + // relay it to peers. + if len(tx) > maxTxSize { + return ErrTxTooLarge + } + + if mem.preCheck != nil { + if err := mem.preCheck(tx); err != nil { + return ErrPreCheck{err} + } + } + + // CACHE + if !mem.cache.Push(tx) { + // Record a new sender for a tx we've already seen. + // Note it's possible a tx is still in the cache but no longer in the mempool + // (eg. after committing a block, txs are removed from mempool but not cache), + // so we only record the sender for txs still in the mempool. + if e, ok := mem.txsMap.Load(txKey(tx)); ok { + memTx := e.(*clist.CElement).Value.(*mempoolTx) + if _, loaded := memTx.senders.LoadOrStore(txInfo.SenderID, true); loaded { + // TODO: consider punishing peer for dups, + // its non-trivial since invalid txs can become valid, + // but they can spam the same tx with little cost to them atm. + } + } + + return ErrTxInCache + } + // END CACHE + + // WAL + if mem.wal != nil { + // TODO: Notify administrators when WAL fails + _, err := mem.wal.Write([]byte(tx)) + if err != nil { + mem.logger.Error("Error writing to WAL", "err", err) + } + _, err = mem.wal.Write([]byte("\n")) + if err != nil { + mem.logger.Error("Error writing to WAL", "err", err) + } + } + // END WAL + + // NOTE: proxyAppConn may error if tx buffer is full + if err = mem.proxyAppConn.Error(); err != nil { + return err + } + + reqRes := mem.proxyAppConn.CheckTxAsync(tx) + reqRes.SetCallback(mem.reqResCb(tx, txInfo.SenderID, cb)) + + return nil +} + +// Global callback that will be called after every ABCI response. +// Having a single global callback avoids needing to set a callback for each request. +// However, processing the checkTx response requires the peerID (so we can track which txs we heard from who), +// and peerID is not included in the ABCI request, so we have to set request-specific callbacks that +// include this information. If we're not in the midst of a recheck, this function will just return, +// so the request specific callback can do the work. +// When rechecking, we don't need the peerID, so the recheck callback happens here. +func (mem *CListMempool) globalCb(req *abci.Request, res *abci.Response) { + if mem.recheckCursor == nil { + return + } + + mem.metrics.RecheckTimes.Add(1) + mem.resCbRecheck(req, res) + + // update metrics + mem.metrics.Size.Set(float64(mem.Size())) +} + +// Request specific callback that should be set on individual reqRes objects +// to incorporate local information when processing the response. +// This allows us to track the peer that sent us this tx, so we can avoid sending it back to them. +// NOTE: alternatively, we could include this information in the ABCI request itself. +// +// External callers of CheckTx, like the RPC, can also pass an externalCb through here that is called +// when all other response processing is complete. +// +// Used in CheckTxWithInfo to record PeerID who sent us the tx. +func (mem *CListMempool) reqResCb(tx []byte, peerID uint16, externalCb func(*abci.Response)) func(res *abci.Response) { + return func(res *abci.Response) { + if mem.recheckCursor != nil { + // this should never happen + panic("recheck cursor is not nil in reqResCb") + } + + mem.resCbFirstTime(tx, peerID, res) + + // update metrics + mem.metrics.Size.Set(float64(mem.Size())) + + // passed in by the caller of CheckTx, eg. the RPC + if externalCb != nil { + externalCb(res) + } + } +} + +// Called from: +// - resCbFirstTime (lock not held) if tx is valid +func (mem *CListMempool) addTx(memTx *mempoolTx) { + e := mem.txs.PushBack(memTx) + mem.txsMap.Store(txKey(memTx.tx), e) + atomic.AddInt64(&mem.txsBytes, int64(len(memTx.tx))) + mem.metrics.TxSizeBytes.Observe(float64(len(memTx.tx))) +} + +// Called from: +// - Update (lock held) if tx was committed +// - resCbRecheck (lock not held) if tx was invalidated +func (mem *CListMempool) removeTx(tx types.Tx, elem *clist.CElement, removeFromCache bool) { + mem.txs.Remove(elem) + elem.DetachPrev() + mem.txsMap.Delete(txKey(tx)) + atomic.AddInt64(&mem.txsBytes, int64(-len(tx))) + + if removeFromCache { + mem.cache.Remove(tx) + } +} + +// callback, which is called after the app checked the tx for the first time. +// +// The case where the app checks the tx for the second and subsequent times is +// handled by the resCbRecheck callback. +func (mem *CListMempool) resCbFirstTime(tx []byte, peerID uint16, res *abci.Response) { + switch r := res.Value.(type) { + case *abci.Response_CheckTx: + var postCheckErr error + if mem.postCheck != nil { + postCheckErr = mem.postCheck(tx, r.CheckTx) + } + if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { + memTx := &mempoolTx{ + height: mem.height, + gasWanted: r.CheckTx.GasWanted, + tx: tx, + } + memTx.senders.Store(peerID, true) + mem.addTx(memTx) + mem.logger.Info("Added good transaction", + "tx", txID(tx), + "res", r, + "height", memTx.height, + "total", mem.Size(), + ) + mem.notifyTxsAvailable() + } else { + // ignore bad transaction + mem.logger.Info("Rejected bad transaction", "tx", txID(tx), "res", r, "err", postCheckErr) + mem.metrics.FailedTxs.Add(1) + // remove from cache (it might be good later) + mem.cache.Remove(tx) + } + default: + // ignore other messages + } +} + +// callback, which is called after the app rechecked the tx. +// +// The case where the app checks the tx for the first time is handled by the +// resCbFirstTime callback. +func (mem *CListMempool) resCbRecheck(req *abci.Request, res *abci.Response) { + switch r := res.Value.(type) { + case *abci.Response_CheckTx: + tx := req.GetCheckTx().Tx + memTx := mem.recheckCursor.Value.(*mempoolTx) + if !bytes.Equal(tx, memTx.tx) { + panic(fmt.Sprintf( + "Unexpected tx response from proxy during recheck\nExpected %X, got %X", + memTx.tx, + tx)) + } + var postCheckErr error + if mem.postCheck != nil { + postCheckErr = mem.postCheck(tx, r.CheckTx) + } + if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { + // Good, nothing to do. + } else { + // Tx became invalidated due to newly committed block. + mem.logger.Info("Tx is no longer valid", "tx", txID(tx), "res", r, "err", postCheckErr) + // NOTE: we remove tx from the cache because it might be good later + mem.removeTx(tx, mem.recheckCursor, true) + } + if mem.recheckCursor == mem.recheckEnd { + mem.recheckCursor = nil + } else { + mem.recheckCursor = mem.recheckCursor.Next() + } + if mem.recheckCursor == nil { + // Done! + atomic.StoreInt32(&mem.rechecking, 0) + mem.logger.Info("Done rechecking txs") + + // incase the recheck removed all txs + if mem.Size() > 0 { + mem.notifyTxsAvailable() + } + } + default: + // ignore other messages + } +} + +func (mem *CListMempool) TxsAvailable() <-chan struct{} { + return mem.txsAvailable +} + +func (mem *CListMempool) notifyTxsAvailable() { + if mem.Size() == 0 { + panic("notified txs available but mempool is empty!") + } + if mem.txsAvailable != nil && !mem.notifiedTxsAvailable { + // channel cap is 1, so this will send once + mem.notifiedTxsAvailable = true + select { + case mem.txsAvailable <- struct{}{}: + default: + } + } +} + +func (mem *CListMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { + mem.proxyMtx.Lock() + defer mem.proxyMtx.Unlock() + + for atomic.LoadInt32(&mem.rechecking) > 0 { + // TODO: Something better? + time.Sleep(time.Millisecond * 10) + } + + var totalBytes int64 + var totalGas int64 + // TODO: we will get a performance boost if we have a good estimate of avg + // size per tx, and set the initial capacity based off of that. + // txs := make([]types.Tx, 0, cmn.MinInt(mem.txs.Len(), max/mem.avgTxSize)) + txs := make([]types.Tx, 0, mem.txs.Len()) + for e := mem.txs.Front(); e != nil; e = e.Next() { + memTx := e.Value.(*mempoolTx) + // Check total size requirement + aminoOverhead := types.ComputeAminoOverhead(memTx.tx, 1) + if maxBytes > -1 && totalBytes+int64(len(memTx.tx))+aminoOverhead > maxBytes { + return txs + } + totalBytes += int64(len(memTx.tx)) + aminoOverhead + // Check total gas requirement. + // If maxGas is negative, skip this check. + // Since newTotalGas < masGas, which + // must be non-negative, it follows that this won't overflow. + newTotalGas := totalGas + memTx.gasWanted + if maxGas > -1 && newTotalGas > maxGas { + return txs + } + totalGas = newTotalGas + txs = append(txs, memTx.tx) + } + return txs +} + +func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { + mem.proxyMtx.Lock() + defer mem.proxyMtx.Unlock() + + if max < 0 { + max = mem.txs.Len() + } + + for atomic.LoadInt32(&mem.rechecking) > 0 { + // TODO: Something better? + time.Sleep(time.Millisecond * 10) + } + + txs := make([]types.Tx, 0, cmn.MinInt(mem.txs.Len(), max)) + for e := mem.txs.Front(); e != nil && len(txs) <= max; e = e.Next() { + memTx := e.Value.(*mempoolTx) + txs = append(txs, memTx.tx) + } + return txs +} + +func (mem *CListMempool) Update( + height int64, + txs types.Txs, + preCheck PreCheckFunc, + postCheck PostCheckFunc, +) error { + // Set height + mem.height = height + mem.notifiedTxsAvailable = false + + if preCheck != nil { + mem.preCheck = preCheck + } + if postCheck != nil { + mem.postCheck = postCheck + } + + // Add committed transactions to cache (if missing). + for _, tx := range txs { + _ = mem.cache.Push(tx) + } + + // Remove committed transactions. + txsLeft := mem.removeTxs(txs) + + // Either recheck non-committed txs to see if they became invalid + // or just notify there're some txs left. + if len(txsLeft) > 0 { + if mem.config.Recheck { + mem.logger.Info("Recheck txs", "numtxs", len(txsLeft), "height", height) + mem.recheckTxs(txsLeft) + // At this point, mem.txs are being rechecked. + // mem.recheckCursor re-scans mem.txs and possibly removes some txs. + // Before mem.Reap(), we should wait for mem.recheckCursor to be nil. + } else { + mem.notifyTxsAvailable() + } + } + + // Update metrics + mem.metrics.Size.Set(float64(mem.Size())) + + return nil +} + +func (mem *CListMempool) removeTxs(txs types.Txs) []types.Tx { + // Build a map for faster lookups. + txsMap := make(map[string]struct{}, len(txs)) + for _, tx := range txs { + txsMap[string(tx)] = struct{}{} + } + + txsLeft := make([]types.Tx, 0, mem.txs.Len()) + for e := mem.txs.Front(); e != nil; e = e.Next() { + memTx := e.Value.(*mempoolTx) + // Remove the tx if it's already in a block. + if _, ok := txsMap[string(memTx.tx)]; ok { + // NOTE: we don't remove committed txs from the cache. + mem.removeTx(memTx.tx, e, false) + + continue + } + txsLeft = append(txsLeft, memTx.tx) + } + return txsLeft +} + +// NOTE: pass in txs because mem.txs can mutate concurrently. +func (mem *CListMempool) recheckTxs(txs []types.Tx) { + if len(txs) == 0 { + return + } + atomic.StoreInt32(&mem.rechecking, 1) + mem.recheckCursor = mem.txs.Front() + mem.recheckEnd = mem.txs.Back() + + // Push txs to proxyAppConn + // NOTE: globalCb may be called concurrently. + for _, tx := range txs { + mem.proxyAppConn.CheckTxAsync(tx) + } + mem.proxyAppConn.FlushAsync() +} + +//-------------------------------------------------------------------------------- + +// mempoolTx is a transaction that successfully ran +type mempoolTx struct { + height int64 // height that this tx had been validated in + gasWanted int64 // amount of gas this tx states it will require + tx types.Tx // + + // ids of peers who've sent us this tx (as a map for quick lookups). + // senders: PeerID -> bool + senders sync.Map +} + +// Height returns the height for this transaction +func (memTx *mempoolTx) Height() int64 { + return atomic.LoadInt64(&memTx.height) +} + +//-------------------------------------------------------------------------------- + +type txCache interface { + Reset() + Push(tx types.Tx) bool + Remove(tx types.Tx) +} + +// mapTxCache maintains a LRU cache of transactions. This only stores the hash +// of the tx, due to memory concerns. +type mapTxCache struct { + mtx sync.Mutex + size int + map_ map[[sha256.Size]byte]*list.Element + list *list.List +} + +var _ txCache = (*mapTxCache)(nil) + +// newMapTxCache returns a new mapTxCache. +func newMapTxCache(cacheSize int) *mapTxCache { + return &mapTxCache{ + size: cacheSize, + map_: make(map[[sha256.Size]byte]*list.Element, cacheSize), + list: list.New(), + } +} + +// Reset resets the cache to an empty state. +func (cache *mapTxCache) Reset() { + cache.mtx.Lock() + cache.map_ = make(map[[sha256.Size]byte]*list.Element, cache.size) + cache.list.Init() + cache.mtx.Unlock() +} + +// Push adds the given tx to the cache and returns true. It returns +// false if tx is already in the cache. +func (cache *mapTxCache) Push(tx types.Tx) bool { + cache.mtx.Lock() + defer cache.mtx.Unlock() + + // Use the tx hash in the cache + txHash := txKey(tx) + if moved, exists := cache.map_[txHash]; exists { + cache.list.MoveToBack(moved) + return false + } + + if cache.list.Len() >= cache.size { + popped := cache.list.Front() + poppedTxHash := popped.Value.([sha256.Size]byte) + delete(cache.map_, poppedTxHash) + if popped != nil { + cache.list.Remove(popped) + } + } + e := cache.list.PushBack(txHash) + cache.map_[txHash] = e + return true +} + +// Remove removes the given tx from the cache. +func (cache *mapTxCache) Remove(tx types.Tx) { + cache.mtx.Lock() + txHash := txKey(tx) + popped := cache.map_[txHash] + delete(cache.map_, txHash) + if popped != nil { + cache.list.Remove(popped) + } + + cache.mtx.Unlock() +} + +type nopTxCache struct{} + +var _ txCache = (*nopTxCache)(nil) + +func (nopTxCache) Reset() {} +func (nopTxCache) Push(types.Tx) bool { return true } +func (nopTxCache) Remove(types.Tx) {} + +//-------------------------------------------------------------------------------- + +// txKey is the fixed length array sha256 hash used as the key in maps. +func txKey(tx types.Tx) [sha256.Size]byte { + return sha256.Sum256(tx) +} + +// txID is the hex encoded hash of the bytes as a types.Tx. +func txID(tx []byte) string { + return fmt.Sprintf("%X", types.Tx(tx).Hash()) +} diff --git a/mempool/mempool_test.go b/mempool/clist_mempool_test.go similarity index 96% rename from mempool/mempool_test.go rename to mempool/clist_mempool_test.go index d5f25396d..69d4eb68c 100644 --- a/mempool/mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -32,18 +32,18 @@ import ( // test. type cleanupFunc func() -func newMempoolWithApp(cc proxy.ClientCreator) (*Mempool, cleanupFunc) { +func newMempoolWithApp(cc proxy.ClientCreator) (*CListMempool, cleanupFunc) { return newMempoolWithAppAndConfig(cc, cfg.ResetTestRoot("mempool_test")) } -func newMempoolWithAppAndConfig(cc proxy.ClientCreator, config *cfg.Config) (*Mempool, cleanupFunc) { +func newMempoolWithAppAndConfig(cc proxy.ClientCreator, config *cfg.Config) (*CListMempool, cleanupFunc) { appConnMem, _ := cc.NewABCIClient() appConnMem.SetLogger(log.TestingLogger().With("module", "abci-client", "connection", "mempool")) err := appConnMem.Start() if err != nil { panic(err) } - mempool := NewMempool(config.Mempool, appConnMem, 0) + mempool := NewCListMempool(config.Mempool, appConnMem, 0) mempool.SetLogger(log.TestingLogger()) return mempool, func() { os.RemoveAll(config.RootDir) } } @@ -66,9 +66,9 @@ func ensureFire(t *testing.T, ch <-chan struct{}, timeoutMS int) { } } -func checkTxs(t *testing.T, mempool *Mempool, count int, peerID uint16) types.Txs { +func checkTxs(t *testing.T, mempool Mempool, count int, peerID uint16) types.Txs { txs := make(types.Txs, count) - txInfo := TxInfo{PeerID: peerID} + txInfo := TxInfo{SenderID: peerID} for i := 0; i < count; i++ { txBytes := make([]byte, 20) txs[i] = txBytes @@ -348,7 +348,6 @@ func TestMempoolCloseWAL(t *testing.T) { // 1. Create the temporary directory for mempool and WAL testing. rootDir, err := ioutil.TempDir("", "mempool-test") require.Nil(t, err, "expecting successful tmpdir creation") - defer os.RemoveAll(rootDir) // 2. Ensure that it doesn't contain any elements -- Sanity check m1, err := filepath.Glob(filepath.Join(rootDir, "*")) @@ -356,13 +355,13 @@ func TestMempoolCloseWAL(t *testing.T) { require.Equal(t, 0, len(m1), "no matches yet") // 3. Create the mempool - wcfg := cfg.DefaultMempoolConfig() - wcfg.RootDir = rootDir - defer os.RemoveAll(wcfg.RootDir) + wcfg := cfg.DefaultConfig() + wcfg.Mempool.RootDir = rootDir app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) - appConnMem, _ := cc.NewABCIClient() - mempool := NewMempool(wcfg, appConnMem, 10) + mempool, cleanup := newMempoolWithAppAndConfig(cc, wcfg) + defer cleanup() + mempool.height = 10 mempool.InitWAL() // 4. Ensure that the directory contains the WAL file @@ -542,7 +541,7 @@ func TestMempoolRemoteAppConcurrency(t *testing.T) { tx := txs[int(txNum)] // this will err with ErrTxInCache many times ... - mempool.CheckTxWithInfo(tx, nil, TxInfo{PeerID: uint16(peerID)}) + mempool.CheckTxWithInfo(tx, nil, TxInfo{SenderID: uint16(peerID)}) } err := mempool.FlushAppConn() require.NoError(t, err) diff --git a/mempool/doc.go b/mempool/doc.go new file mode 100644 index 000000000..ddd47aa2d --- /dev/null +++ b/mempool/doc.go @@ -0,0 +1,24 @@ +// The mempool pushes new txs onto the proxyAppConn. +// It gets a stream of (req, res) tuples from the proxy. +// The mempool stores good txs in a concurrent linked-list. + +// Multiple concurrent go-routines can traverse this linked-list +// safely by calling .NextWait() on each element. + +// So we have several go-routines: +// 1. Consensus calling Update() and Reap() synchronously +// 2. Many mempool reactor's peer routines calling CheckTx() +// 3. Many mempool reactor's peer routines traversing the txs linked list +// 4. Another goroutine calling GarbageCollectTxs() periodically + +// To manage these goroutines, there are three methods of locking. +// 1. Mutations to the linked-list is protected by an internal mtx (CList is goroutine-safe) +// 2. Mutations to the linked-list elements are atomic +// 3. CheckTx() calls can be paused upon Update() and Reap(), protected by .proxyMtx + +// Garbage collection of old elements from mempool.txs is handlde via +// the DetachPrev() call, which makes old elements not reachable by +// peer broadcastTxRoutine() automatically garbage collected. + +// TODO: Better handle abci client errors. (make it automatically handle connection errors) +package mempool diff --git a/mempool/errors.go b/mempool/errors.go new file mode 100644 index 000000000..ac2a9b3c2 --- /dev/null +++ b/mempool/errors.go @@ -0,0 +1,46 @@ +package mempool + +import ( + "fmt" + + "github.com/pkg/errors" +) + +var ( + // ErrTxInCache is returned to the client if we saw tx earlier + ErrTxInCache = errors.New("Tx already exists in cache") + + // ErrTxTooLarge means the tx is too big to be sent in a message to other peers + ErrTxTooLarge = fmt.Errorf("Tx too large. Max size is %d", maxTxSize) +) + +// ErrMempoolIsFull means Tendermint & an application can't handle that much load +type ErrMempoolIsFull struct { + numTxs int + maxTxs int + + txsBytes int64 + maxTxsBytes int64 +} + +func (e ErrMempoolIsFull) Error() string { + return fmt.Sprintf( + "mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", + e.numTxs, e.maxTxs, + e.txsBytes, e.maxTxsBytes) +} + +// ErrPreCheck is returned when tx is too big +type ErrPreCheck struct { + Reason error +} + +func (e ErrPreCheck) Error() string { + return e.Reason.Error() +} + +// IsPreCheckError returns true if err is due to pre check failure. +func IsPreCheckError(err error) bool { + _, ok := err.(ErrPreCheck) + return ok +} diff --git a/mempool/mempool.go b/mempool/mempool.go index a5b14466a..1c1873d47 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -1,111 +1,102 @@ package mempool import ( - "bytes" - "container/list" - "crypto/sha256" "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/pkg/errors" abci "github.com/tendermint/tendermint/abci/types" - cfg "github.com/tendermint/tendermint/config" - auto "github.com/tendermint/tendermint/libs/autofile" - "github.com/tendermint/tendermint/libs/clist" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" ) -// PreCheckFunc is an optional filter executed before CheckTx and rejects -// transaction if false is returned. An example would be to ensure that a -// transaction doesn't exceeded the block size. -type PreCheckFunc func(types.Tx) error +// Mempool defines the mempool interface. +// +// Updates to the mempool need to be synchronized with committing a block so +// apps can reset their transient state on Commit. +type Mempool interface { + // CheckTx executes a new transaction against the application to determine + // its validity and whether it should be added to the mempool. + CheckTx(tx types.Tx, callback func(*abci.Response)) error -// PostCheckFunc is an optional filter executed after CheckTx and rejects -// transaction if false is returned. An example would be to ensure a -// transaction doesn't require more gas than available for the block. -type PostCheckFunc func(types.Tx, *abci.ResponseCheckTx) error + // CheckTxWithInfo performs the same operation as CheckTx, but with extra + // meta data about the tx. + // Currently this metadata is the peer who sent it, used to prevent the tx + // from being gossiped back to them. + CheckTxWithInfo(tx types.Tx, callback func(*abci.Response), txInfo TxInfo) error -// TxInfo are parameters that get passed when attempting to add a tx to the -// mempool. -type TxInfo struct { - // We don't use p2p.ID here because it's too big. The gain is to store max 2 - // bytes with each tx to identify the sender rather than 20 bytes. - PeerID uint16 -} + // ReapMaxBytesMaxGas reaps transactions from the mempool up to maxBytes + // bytes total with the condition that the total gasWanted must be less than + // maxGas. + // If both maxes are negative, there is no cap on the size of all returned + // transactions (~ all available transactions). + ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs -/* + // ReapMaxTxs reaps up to max transactions from the mempool. + // If max is negative, there is no cap on the size of all returned + // transactions (~ all available transactions). + ReapMaxTxs(max int) types.Txs -The mempool pushes new txs onto the proxyAppConn. -It gets a stream of (req, res) tuples from the proxy. -The mempool stores good txs in a concurrent linked-list. + // Lock locks the mempool. The consensus must be able to hold lock to safely update. + Lock() -Multiple concurrent go-routines can traverse this linked-list -safely by calling .NextWait() on each element. + // Unlock unlocks the mempool. + Unlock() -So we have several go-routines: -1. Consensus calling Update() and Reap() synchronously -2. Many mempool reactor's peer routines calling CheckTx() -3. Many mempool reactor's peer routines traversing the txs linked list -4. Another goroutine calling GarbageCollectTxs() periodically + // Update informs the mempool that the given txs were committed and can be discarded. + // NOTE: this should be called *after* block is committed by consensus. + // NOTE: unsafe; Lock/Unlock must be managed by caller + Update(blockHeight int64, blockTxs types.Txs, newPreFn PreCheckFunc, newPostFn PostCheckFunc) error -To manage these goroutines, there are three methods of locking. -1. Mutations to the linked-list is protected by an internal mtx (CList is goroutine-safe) -2. Mutations to the linked-list elements are atomic -3. CheckTx() calls can be paused upon Update() and Reap(), protected by .proxyMtx + // FlushAppConn flushes the mempool connection to ensure async reqResCb calls are + // done. E.g. from CheckTx. + FlushAppConn() error -Garbage collection of old elements from mempool.txs is handlde via -the DetachPrev() call, which makes old elements not reachable by -peer broadcastTxRoutine() automatically garbage collected. + // Flush removes all transactions from the mempool and cache + Flush() -TODO: Better handle abci client errors. (make it automatically handle connection errors) + // TxsAvailable returns a channel which fires once for every height, + // and only when transactions are available in the mempool. + // NOTE: the returned channel may be nil if EnableTxsAvailable was not called. + TxsAvailable() <-chan struct{} -*/ + // EnableTxsAvailable initializes the TxsAvailable channel, ensuring it will + // trigger once every height when transactions are available. + EnableTxsAvailable() -var ( - // ErrTxInCache is returned to the client if we saw tx earlier - ErrTxInCache = errors.New("Tx already exists in cache") + // Size returns the number of transactions in the mempool. + Size() int - // ErrTxTooLarge means the tx is too big to be sent in a message to other peers - ErrTxTooLarge = fmt.Errorf("Tx too large. Max size is %d", maxTxSize) -) + // TxsBytes returns the total size of all txs in the mempool. + TxsBytes() int64 -// ErrMempoolIsFull means Tendermint & an application can't handle that much load -type ErrMempoolIsFull struct { - numTxs int - maxTxs int + // InitWAL creates a directory for the WAL file and opens a file itself. + InitWAL() - txsBytes int64 - maxTxsBytes int64 + // CloseWAL closes and discards the underlying WAL file. + // Any further writes will not be relayed to disk. + CloseWAL() } -func (e ErrMempoolIsFull) Error() string { - return fmt.Sprintf( - "Mempool is full: number of txs %d (max: %d), total txs bytes %d (max: %d)", - e.numTxs, e.maxTxs, - e.txsBytes, e.maxTxsBytes) -} +//-------------------------------------------------------------------------------- -// ErrPreCheck is returned when tx is too big -type ErrPreCheck struct { - Reason error -} +// PreCheckFunc is an optional filter executed before CheckTx and rejects +// transaction if false is returned. An example would be to ensure that a +// transaction doesn't exceeded the block size. +type PreCheckFunc func(types.Tx) error -func (e ErrPreCheck) Error() string { - return e.Reason.Error() -} +// PostCheckFunc is an optional filter executed after CheckTx and rejects +// transaction if false is returned. An example would be to ensure a +// transaction doesn't require more gas than available for the block. +type PostCheckFunc func(types.Tx, *abci.ResponseCheckTx) error -// IsPreCheckError returns true if err is due to pre check failure. -func IsPreCheckError(err error) bool { - _, ok := err.(ErrPreCheck) - return ok +// TxInfo are parameters that get passed when attempting to add a tx to the +// mempool. +type TxInfo struct { + // We don't use p2p.ID here because it's too big. The gain is to store max 2 + // bytes with each tx to identify the sender rather than 20 bytes. + SenderID uint16 } +//-------------------------------------------------------------------------------- + // PreCheckAminoMaxBytes checks that the size of the transaction plus the amino // overhead is smaller or equal to the expected maxBytes. func PreCheckAminoMaxBytes(maxBytes int64) PreCheckFunc { @@ -143,718 +134,3 @@ func PostCheckMaxGas(maxGas int64) PostCheckFunc { return nil } } - -// TxID is the hex encoded hash of the bytes as a types.Tx. -func TxID(tx []byte) string { - return fmt.Sprintf("%X", types.Tx(tx).Hash()) -} - -// txKey is the fixed length array sha256 hash used as the key in maps. -func txKey(tx types.Tx) [sha256.Size]byte { - return sha256.Sum256(tx) -} - -// Mempool is an ordered in-memory pool for transactions before they are proposed in a consensus -// round. Transaction validity is checked using the CheckTx abci message before the transaction is -// added to the pool. The Mempool uses a concurrent list structure for storing transactions that -// can be efficiently accessed by multiple concurrent readers. -type Mempool struct { - config *cfg.MempoolConfig - - proxyMtx sync.Mutex - proxyAppConn proxy.AppConnMempool - txs *clist.CList // concurrent linked-list of good txs - preCheck PreCheckFunc - postCheck PostCheckFunc - - // Track whether we're rechecking txs. - // These are not protected by a mutex and are expected to be mutated - // in serial (ie. by abci responses which are called in serial). - recheckCursor *clist.CElement // next expected response - recheckEnd *clist.CElement // re-checking stops here - - // notify listeners (ie. consensus) when txs are available - notifiedTxsAvailable bool - txsAvailable chan struct{} // fires once for each height, when the mempool is not empty - - // Map for quick access to txs to record sender in CheckTx. - // txsMap: txKey -> CElement - txsMap sync.Map - - // Atomic integers - height int64 // the last block Update()'d to - rechecking int32 // for re-checking filtered txs on Update() - txsBytes int64 // total size of mempool, in bytes - - // Keep a cache of already-seen txs. - // This reduces the pressure on the proxyApp. - cache txCache - - // A log of mempool txs - wal *auto.AutoFile - - logger log.Logger - - metrics *Metrics -} - -// MempoolOption sets an optional parameter on the Mempool. -type MempoolOption func(*Mempool) - -// NewMempool returns a new Mempool with the given configuration and connection to an application. -func NewMempool( - config *cfg.MempoolConfig, - proxyAppConn proxy.AppConnMempool, - height int64, - options ...MempoolOption, -) *Mempool { - mempool := &Mempool{ - config: config, - proxyAppConn: proxyAppConn, - txs: clist.New(), - height: height, - rechecking: 0, - recheckCursor: nil, - recheckEnd: nil, - logger: log.NewNopLogger(), - metrics: NopMetrics(), - } - if config.CacheSize > 0 { - mempool.cache = newMapTxCache(config.CacheSize) - } else { - mempool.cache = nopTxCache{} - } - proxyAppConn.SetResponseCallback(mempool.globalCb) - for _, option := range options { - option(mempool) - } - return mempool -} - -// EnableTxsAvailable initializes the TxsAvailable channel, -// ensuring it will trigger once every height when transactions are available. -// NOTE: not thread safe - should only be called once, on startup -func (mem *Mempool) EnableTxsAvailable() { - mem.txsAvailable = make(chan struct{}, 1) -} - -// SetLogger sets the Logger. -func (mem *Mempool) SetLogger(l log.Logger) { - mem.logger = l -} - -// WithPreCheck sets a filter for the mempool to reject a tx if f(tx) returns -// false. This is ran before CheckTx. -func WithPreCheck(f PreCheckFunc) MempoolOption { - return func(mem *Mempool) { mem.preCheck = f } -} - -// WithPostCheck sets a filter for the mempool to reject a tx if f(tx) returns -// false. This is ran after CheckTx. -func WithPostCheck(f PostCheckFunc) MempoolOption { - return func(mem *Mempool) { mem.postCheck = f } -} - -// WithMetrics sets the metrics. -func WithMetrics(metrics *Metrics) MempoolOption { - return func(mem *Mempool) { mem.metrics = metrics } -} - -// InitWAL creates a directory for the WAL file and opens a file itself. -// -// *panics* if can't create directory or open file. -// *not thread safe* -func (mem *Mempool) InitWAL() { - walDir := mem.config.WalDir() - err := cmn.EnsureDir(walDir, 0700) - if err != nil { - panic(errors.Wrap(err, "Error ensuring Mempool WAL dir")) - } - af, err := auto.OpenAutoFile(walDir + "/wal") - if err != nil { - panic(errors.Wrap(err, "Error opening Mempool WAL file")) - } - mem.wal = af -} - -// CloseWAL closes and discards the underlying WAL file. -// Any further writes will not be relayed to disk. -func (mem *Mempool) CloseWAL() { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() - - if err := mem.wal.Close(); err != nil { - mem.logger.Error("Error closing WAL", "err", err) - } - mem.wal = nil -} - -// Lock locks the mempool. The consensus must be able to hold lock to safely update. -func (mem *Mempool) Lock() { - mem.proxyMtx.Lock() -} - -// Unlock unlocks the mempool. -func (mem *Mempool) Unlock() { - mem.proxyMtx.Unlock() -} - -// Size returns the number of transactions in the mempool. -func (mem *Mempool) Size() int { - return mem.txs.Len() -} - -// TxsBytes returns the total size of all txs in the mempool. -func (mem *Mempool) TxsBytes() int64 { - return atomic.LoadInt64(&mem.txsBytes) -} - -// FlushAppConn flushes the mempool connection to ensure async reqResCb calls are -// done. E.g. from CheckTx. -func (mem *Mempool) FlushAppConn() error { - return mem.proxyAppConn.FlushSync() -} - -// Flush removes all transactions from the mempool and cache -func (mem *Mempool) Flush() { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() - - mem.cache.Reset() - - for e := mem.txs.Front(); e != nil; e = e.Next() { - mem.txs.Remove(e) - e.DetachPrev() - } - - mem.txsMap = sync.Map{} - _ = atomic.SwapInt64(&mem.txsBytes, 0) -} - -// TxsFront returns the first transaction in the ordered list for peer -// goroutines to call .NextWait() on. -func (mem *Mempool) TxsFront() *clist.CElement { - return mem.txs.Front() -} - -// TxsWaitChan returns a channel to wait on transactions. It will be closed -// once the mempool is not empty (ie. the internal `mem.txs` has at least one -// element) -func (mem *Mempool) TxsWaitChan() <-chan struct{} { - return mem.txs.WaitChan() -} - -// CheckTx executes a new transaction against the application to determine its validity -// and whether it should be added to the mempool. -// It blocks if we're waiting on Update() or Reap(). -// cb: A callback from the CheckTx command. -// It gets called from another goroutine. -// CONTRACT: Either cb will get called, or err returned. -func (mem *Mempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err error) { - return mem.CheckTxWithInfo(tx, cb, TxInfo{PeerID: UnknownPeerID}) -} - -// CheckTxWithInfo performs the same operation as CheckTx, but with extra meta data about the tx. -// Currently this metadata is the peer who sent it, -// used to prevent the tx from being gossiped back to them. -func (mem *Mempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), txInfo TxInfo) (err error) { - mem.proxyMtx.Lock() - // use defer to unlock mutex because application (*local client*) might panic - defer mem.proxyMtx.Unlock() - - var ( - memSize = mem.Size() - txsBytes = mem.TxsBytes() - ) - if memSize >= mem.config.Size || - int64(len(tx))+txsBytes > mem.config.MaxTxsBytes { - return ErrMempoolIsFull{ - memSize, mem.config.Size, - txsBytes, mem.config.MaxTxsBytes} - } - - // The size of the corresponding amino-encoded TxMessage - // can't be larger than the maxMsgSize, otherwise we can't - // relay it to peers. - if len(tx) > maxTxSize { - return ErrTxTooLarge - } - - if mem.preCheck != nil { - if err := mem.preCheck(tx); err != nil { - return ErrPreCheck{err} - } - } - - // CACHE - if !mem.cache.Push(tx) { - // Record a new sender for a tx we've already seen. - // Note it's possible a tx is still in the cache but no longer in the mempool - // (eg. after committing a block, txs are removed from mempool but not cache), - // so we only record the sender for txs still in the mempool. - if e, ok := mem.txsMap.Load(txKey(tx)); ok { - memTx := e.(*clist.CElement).Value.(*mempoolTx) - if _, loaded := memTx.senders.LoadOrStore(txInfo.PeerID, true); loaded { - // TODO: consider punishing peer for dups, - // its non-trivial since invalid txs can become valid, - // but they can spam the same tx with little cost to them atm. - } - } - - return ErrTxInCache - } - // END CACHE - - // WAL - if mem.wal != nil { - // TODO: Notify administrators when WAL fails - _, err := mem.wal.Write([]byte(tx)) - if err != nil { - mem.logger.Error("Error writing to WAL", "err", err) - } - _, err = mem.wal.Write([]byte("\n")) - if err != nil { - mem.logger.Error("Error writing to WAL", "err", err) - } - } - // END WAL - - // NOTE: proxyAppConn may error if tx buffer is full - if err = mem.proxyAppConn.Error(); err != nil { - return err - } - - reqRes := mem.proxyAppConn.CheckTxAsync(tx) - reqRes.SetCallback(mem.reqResCb(tx, txInfo.PeerID, cb)) - - return nil -} - -// Global callback that will be called after every ABCI response. -// Having a single global callback avoids needing to set a callback for each request. -// However, processing the checkTx response requires the peerID (so we can track which txs we heard from who), -// and peerID is not included in the ABCI request, so we have to set request-specific callbacks that -// include this information. If we're not in the midst of a recheck, this function will just return, -// so the request specific callback can do the work. -// When rechecking, we don't need the peerID, so the recheck callback happens here. -func (mem *Mempool) globalCb(req *abci.Request, res *abci.Response) { - if mem.recheckCursor == nil { - return - } - - mem.metrics.RecheckTimes.Add(1) - mem.resCbRecheck(req, res) - - // update metrics - mem.metrics.Size.Set(float64(mem.Size())) -} - -// Request specific callback that should be set on individual reqRes objects -// to incorporate local information when processing the response. -// This allows us to track the peer that sent us this tx, so we can avoid sending it back to them. -// NOTE: alternatively, we could include this information in the ABCI request itself. -// -// External callers of CheckTx, like the RPC, can also pass an externalCb through here that is called -// when all other response processing is complete. -// -// Used in CheckTxWithInfo to record PeerID who sent us the tx. -func (mem *Mempool) reqResCb(tx []byte, peerID uint16, externalCb func(*abci.Response)) func(res *abci.Response) { - return func(res *abci.Response) { - if mem.recheckCursor != nil { - // this should never happen - panic("recheck cursor is not nil in reqResCb") - } - - mem.resCbFirstTime(tx, peerID, res) - - // update metrics - mem.metrics.Size.Set(float64(mem.Size())) - - // passed in by the caller of CheckTx, eg. the RPC - if externalCb != nil { - externalCb(res) - } - } -} - -// Called from: -// - resCbFirstTime (lock not held) if tx is valid -func (mem *Mempool) addTx(memTx *mempoolTx) { - e := mem.txs.PushBack(memTx) - mem.txsMap.Store(txKey(memTx.tx), e) - atomic.AddInt64(&mem.txsBytes, int64(len(memTx.tx))) - mem.metrics.TxSizeBytes.Observe(float64(len(memTx.tx))) -} - -// Called from: -// - Update (lock held) if tx was committed -// - resCbRecheck (lock not held) if tx was invalidated -func (mem *Mempool) removeTx(tx types.Tx, elem *clist.CElement, removeFromCache bool) { - mem.txs.Remove(elem) - elem.DetachPrev() - mem.txsMap.Delete(txKey(tx)) - atomic.AddInt64(&mem.txsBytes, int64(-len(tx))) - - if removeFromCache { - mem.cache.Remove(tx) - } -} - -// callback, which is called after the app checked the tx for the first time. -// -// The case where the app checks the tx for the second and subsequent times is -// handled by the resCbRecheck callback. -func (mem *Mempool) resCbFirstTime(tx []byte, peerID uint16, res *abci.Response) { - switch r := res.Value.(type) { - case *abci.Response_CheckTx: - var postCheckErr error - if mem.postCheck != nil { - postCheckErr = mem.postCheck(tx, r.CheckTx) - } - if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { - memTx := &mempoolTx{ - height: mem.height, - gasWanted: r.CheckTx.GasWanted, - tx: tx, - } - memTx.senders.Store(peerID, true) - mem.addTx(memTx) - mem.logger.Info("Added good transaction", - "tx", TxID(tx), - "res", r, - "height", memTx.height, - "total", mem.Size(), - ) - mem.notifyTxsAvailable() - } else { - // ignore bad transaction - mem.logger.Info("Rejected bad transaction", "tx", TxID(tx), "res", r, "err", postCheckErr) - mem.metrics.FailedTxs.Add(1) - // remove from cache (it might be good later) - mem.cache.Remove(tx) - } - default: - // ignore other messages - } -} - -// callback, which is called after the app rechecked the tx. -// -// The case where the app checks the tx for the first time is handled by the -// resCbFirstTime callback. -func (mem *Mempool) resCbRecheck(req *abci.Request, res *abci.Response) { - switch r := res.Value.(type) { - case *abci.Response_CheckTx: - tx := req.GetCheckTx().Tx - memTx := mem.recheckCursor.Value.(*mempoolTx) - if !bytes.Equal(tx, memTx.tx) { - panic(fmt.Sprintf( - "Unexpected tx response from proxy during recheck\nExpected %X, got %X", - memTx.tx, - tx)) - } - var postCheckErr error - if mem.postCheck != nil { - postCheckErr = mem.postCheck(tx, r.CheckTx) - } - if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { - // Good, nothing to do. - } else { - // Tx became invalidated due to newly committed block. - mem.logger.Info("Tx is no longer valid", "tx", TxID(tx), "res", r, "err", postCheckErr) - // NOTE: we remove tx from the cache because it might be good later - mem.removeTx(tx, mem.recheckCursor, true) - } - if mem.recheckCursor == mem.recheckEnd { - mem.recheckCursor = nil - } else { - mem.recheckCursor = mem.recheckCursor.Next() - } - if mem.recheckCursor == nil { - // Done! - atomic.StoreInt32(&mem.rechecking, 0) - mem.logger.Info("Done rechecking txs") - - // incase the recheck removed all txs - if mem.Size() > 0 { - mem.notifyTxsAvailable() - } - } - default: - // ignore other messages - } -} - -// TxsAvailable returns a channel which fires once for every height, -// and only when transactions are available in the mempool. -// NOTE: the returned channel may be nil if EnableTxsAvailable was not called. -func (mem *Mempool) TxsAvailable() <-chan struct{} { - return mem.txsAvailable -} - -func (mem *Mempool) notifyTxsAvailable() { - if mem.Size() == 0 { - panic("notified txs available but mempool is empty!") - } - if mem.txsAvailable != nil && !mem.notifiedTxsAvailable { - // channel cap is 1, so this will send once - mem.notifiedTxsAvailable = true - select { - case mem.txsAvailable <- struct{}{}: - default: - } - } -} - -// ReapMaxBytesMaxGas reaps transactions from the mempool up to maxBytes bytes total -// with the condition that the total gasWanted must be less than maxGas. -// If both maxes are negative, there is no cap on the size of all returned -// transactions (~ all available transactions). -func (mem *Mempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() - - for atomic.LoadInt32(&mem.rechecking) > 0 { - // TODO: Something better? - time.Sleep(time.Millisecond * 10) - } - - var totalBytes int64 - var totalGas int64 - // TODO: we will get a performance boost if we have a good estimate of avg - // size per tx, and set the initial capacity based off of that. - // txs := make([]types.Tx, 0, cmn.MinInt(mem.txs.Len(), max/mem.avgTxSize)) - txs := make([]types.Tx, 0, mem.txs.Len()) - for e := mem.txs.Front(); e != nil; e = e.Next() { - memTx := e.Value.(*mempoolTx) - // Check total size requirement - aminoOverhead := types.ComputeAminoOverhead(memTx.tx, 1) - if maxBytes > -1 && totalBytes+int64(len(memTx.tx))+aminoOverhead > maxBytes { - return txs - } - totalBytes += int64(len(memTx.tx)) + aminoOverhead - // Check total gas requirement. - // If maxGas is negative, skip this check. - // Since newTotalGas < masGas, which - // must be non-negative, it follows that this won't overflow. - newTotalGas := totalGas + memTx.gasWanted - if maxGas > -1 && newTotalGas > maxGas { - return txs - } - totalGas = newTotalGas - txs = append(txs, memTx.tx) - } - return txs -} - -// ReapMaxTxs reaps up to max transactions from the mempool. -// If max is negative, there is no cap on the size of all returned -// transactions (~ all available transactions). -func (mem *Mempool) ReapMaxTxs(max int) types.Txs { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() - - if max < 0 { - max = mem.txs.Len() - } - - for atomic.LoadInt32(&mem.rechecking) > 0 { - // TODO: Something better? - time.Sleep(time.Millisecond * 10) - } - - txs := make([]types.Tx, 0, cmn.MinInt(mem.txs.Len(), max)) - for e := mem.txs.Front(); e != nil && len(txs) <= max; e = e.Next() { - memTx := e.Value.(*mempoolTx) - txs = append(txs, memTx.tx) - } - return txs -} - -// Update informs the mempool that the given txs were committed and can be discarded. -// NOTE: this should be called *after* block is committed by consensus. -// NOTE: unsafe; Lock/Unlock must be managed by caller -func (mem *Mempool) Update( - height int64, - txs types.Txs, - preCheck PreCheckFunc, - postCheck PostCheckFunc, -) error { - // Set height - mem.height = height - mem.notifiedTxsAvailable = false - - if preCheck != nil { - mem.preCheck = preCheck - } - if postCheck != nil { - mem.postCheck = postCheck - } - - // Add committed transactions to cache (if missing). - for _, tx := range txs { - _ = mem.cache.Push(tx) - } - - // Remove committed transactions. - txsLeft := mem.removeTxs(txs) - - // Either recheck non-committed txs to see if they became invalid - // or just notify there're some txs left. - if len(txsLeft) > 0 { - if mem.config.Recheck { - mem.logger.Info("Recheck txs", "numtxs", len(txsLeft), "height", height) - mem.recheckTxs(txsLeft) - // At this point, mem.txs are being rechecked. - // mem.recheckCursor re-scans mem.txs and possibly removes some txs. - // Before mem.Reap(), we should wait for mem.recheckCursor to be nil. - } else { - mem.notifyTxsAvailable() - } - } - - // Update metrics - mem.metrics.Size.Set(float64(mem.Size())) - - return nil -} - -func (mem *Mempool) removeTxs(txs types.Txs) []types.Tx { - // Build a map for faster lookups. - txsMap := make(map[string]struct{}, len(txs)) - for _, tx := range txs { - txsMap[string(tx)] = struct{}{} - } - - txsLeft := make([]types.Tx, 0, mem.txs.Len()) - for e := mem.txs.Front(); e != nil; e = e.Next() { - memTx := e.Value.(*mempoolTx) - // Remove the tx if it's already in a block. - if _, ok := txsMap[string(memTx.tx)]; ok { - // NOTE: we don't remove committed txs from the cache. - mem.removeTx(memTx.tx, e, false) - - continue - } - txsLeft = append(txsLeft, memTx.tx) - } - return txsLeft -} - -// NOTE: pass in txs because mem.txs can mutate concurrently. -func (mem *Mempool) recheckTxs(txs []types.Tx) { - if len(txs) == 0 { - return - } - atomic.StoreInt32(&mem.rechecking, 1) - mem.recheckCursor = mem.txs.Front() - mem.recheckEnd = mem.txs.Back() - - // Push txs to proxyAppConn - // NOTE: globalCb may be called concurrently. - for _, tx := range txs { - mem.proxyAppConn.CheckTxAsync(tx) - } - mem.proxyAppConn.FlushAsync() -} - -//-------------------------------------------------------------------------------- - -// mempoolTx is a transaction that successfully ran -type mempoolTx struct { - height int64 // height that this tx had been validated in - gasWanted int64 // amount of gas this tx states it will require - tx types.Tx // - - // ids of peers who've sent us this tx (as a map for quick lookups). - // senders: PeerID -> bool - senders sync.Map -} - -// Height returns the height for this transaction -func (memTx *mempoolTx) Height() int64 { - return atomic.LoadInt64(&memTx.height) -} - -//-------------------------------------------------------------------------------- - -type txCache interface { - Reset() - Push(tx types.Tx) bool - Remove(tx types.Tx) -} - -// mapTxCache maintains a LRU cache of transactions. This only stores the hash -// of the tx, due to memory concerns. -type mapTxCache struct { - mtx sync.Mutex - size int - map_ map[[sha256.Size]byte]*list.Element - list *list.List -} - -var _ txCache = (*mapTxCache)(nil) - -// newMapTxCache returns a new mapTxCache. -func newMapTxCache(cacheSize int) *mapTxCache { - return &mapTxCache{ - size: cacheSize, - map_: make(map[[sha256.Size]byte]*list.Element, cacheSize), - list: list.New(), - } -} - -// Reset resets the cache to an empty state. -func (cache *mapTxCache) Reset() { - cache.mtx.Lock() - cache.map_ = make(map[[sha256.Size]byte]*list.Element, cache.size) - cache.list.Init() - cache.mtx.Unlock() -} - -// Push adds the given tx to the cache and returns true. It returns -// false if tx is already in the cache. -func (cache *mapTxCache) Push(tx types.Tx) bool { - cache.mtx.Lock() - defer cache.mtx.Unlock() - - // Use the tx hash in the cache - txHash := txKey(tx) - if moved, exists := cache.map_[txHash]; exists { - cache.list.MoveToBack(moved) - return false - } - - if cache.list.Len() >= cache.size { - popped := cache.list.Front() - poppedTxHash := popped.Value.([sha256.Size]byte) - delete(cache.map_, poppedTxHash) - if popped != nil { - cache.list.Remove(popped) - } - } - e := cache.list.PushBack(txHash) - cache.map_[txHash] = e - return true -} - -// Remove removes the given tx from the cache. -func (cache *mapTxCache) Remove(tx types.Tx) { - cache.mtx.Lock() - txHash := txKey(tx) - popped := cache.map_[txHash] - delete(cache.map_, txHash) - if popped != nil { - cache.list.Remove(popped) - } - - cache.mtx.Unlock() -} - -type nopTxCache struct{} - -var _ txCache = (*nopTxCache)(nil) - -func (nopTxCache) Reset() {} -func (nopTxCache) Push(types.Tx) bool { return true } -func (nopTxCache) Remove(types.Tx) {} diff --git a/mempool/reactor.go b/mempool/reactor.go index e1376b287..65ccd7dfd 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -31,13 +31,13 @@ const ( maxActiveIDs = math.MaxUint16 ) -// MempoolReactor handles mempool tx broadcasting amongst peers. +// Reactor handles mempool tx broadcasting amongst peers. // It maintains a map from peer ID to counter, to prevent gossiping txs to the // peers you received it from. -type MempoolReactor struct { +type Reactor struct { p2p.BaseReactor config *cfg.MempoolConfig - Mempool *Mempool + mempool *CListMempool ids *mempoolIDs } @@ -104,25 +104,25 @@ func newMempoolIDs() *mempoolIDs { } } -// NewMempoolReactor returns a new MempoolReactor with the given config and mempool. -func NewMempoolReactor(config *cfg.MempoolConfig, mempool *Mempool) *MempoolReactor { - memR := &MempoolReactor{ +// NewReactor returns a new Reactor with the given config and mempool. +func NewReactor(config *cfg.MempoolConfig, mempool *CListMempool) *Reactor { + memR := &Reactor{ config: config, - Mempool: mempool, + mempool: mempool, ids: newMempoolIDs(), } - memR.BaseReactor = *p2p.NewBaseReactor("MempoolReactor", memR) + memR.BaseReactor = *p2p.NewBaseReactor("Reactor", memR) return memR } -// SetLogger sets the Logger on the reactor and the underlying Mempool. -func (memR *MempoolReactor) SetLogger(l log.Logger) { +// SetLogger sets the Logger on the reactor and the underlying mempool. +func (memR *Reactor) SetLogger(l log.Logger) { memR.Logger = l - memR.Mempool.SetLogger(l) + memR.mempool.SetLogger(l) } // OnStart implements p2p.BaseReactor. -func (memR *MempoolReactor) OnStart() error { +func (memR *Reactor) OnStart() error { if !memR.config.Broadcast { memR.Logger.Info("Tx broadcasting is disabled") } @@ -131,7 +131,7 @@ func (memR *MempoolReactor) OnStart() error { // GetChannels implements Reactor. // It returns the list of channels for this reactor. -func (memR *MempoolReactor) GetChannels() []*p2p.ChannelDescriptor { +func (memR *Reactor) GetChannels() []*p2p.ChannelDescriptor { return []*p2p.ChannelDescriptor{ { ID: MempoolChannel, @@ -142,20 +142,20 @@ func (memR *MempoolReactor) GetChannels() []*p2p.ChannelDescriptor { // AddPeer implements Reactor. // It starts a broadcast routine ensuring all txs are forwarded to the given peer. -func (memR *MempoolReactor) AddPeer(peer p2p.Peer) { +func (memR *Reactor) AddPeer(peer p2p.Peer) { memR.ids.ReserveForPeer(peer) go memR.broadcastTxRoutine(peer) } // RemovePeer implements Reactor. -func (memR *MempoolReactor) RemovePeer(peer p2p.Peer, reason interface{}) { +func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { memR.ids.Reclaim(peer) // broadcast routine checks if peer is gone and returns } // Receive implements Reactor. // It adds any received transactions to the mempool. -func (memR *MempoolReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { +func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { msg, err := decodeMsg(msgBytes) if err != nil { memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) @@ -167,9 +167,9 @@ func (memR *MempoolReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { switch msg := msg.(type) { case *TxMessage: peerID := memR.ids.GetForPeer(src) - err := memR.Mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{PeerID: peerID}) + err := memR.mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{SenderID: peerID}) if err != nil { - memR.Logger.Info("Could not check tx", "tx", TxID(msg.Tx), "err", err) + memR.Logger.Info("Could not check tx", "tx", txID(msg.Tx), "err", err) } // broadcasting happens from go routines per peer default: @@ -183,7 +183,7 @@ type PeerState interface { } // Send new mempool txs to peer. -func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { +func (memR *Reactor) broadcastTxRoutine(peer p2p.Peer) { if !memR.config.Broadcast { return } @@ -200,8 +200,8 @@ func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { // start from the beginning. if next == nil { select { - case <-memR.Mempool.TxsWaitChan(): // Wait until a tx is available - if next = memR.Mempool.TxsFront(); next == nil { + case <-memR.mempool.TxsWaitChan(): // Wait until a tx is available + if next = memR.mempool.TxsFront(); next == nil { continue } case <-peer.Quit(): @@ -255,7 +255,7 @@ func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { //----------------------------------------------------------------------------- // Messages -// MempoolMessage is a message sent or received by the MempoolReactor. +// MempoolMessage is a message sent or received by the Reactor. type MempoolMessage interface{} func RegisterMempoolMessages(cdc *amino.Codec) { diff --git a/mempool/reactor_test.go b/mempool/reactor_test.go index c9cf49809..94c0d1900 100644 --- a/mempool/reactor_test.go +++ b/mempool/reactor_test.go @@ -1,7 +1,6 @@ package mempool import ( - "fmt" "net" "sync" "testing" @@ -43,8 +42,8 @@ func mempoolLogger() log.Logger { } // connect N mempool reactors through N switches -func makeAndConnectMempoolReactors(config *cfg.Config, N int) []*MempoolReactor { - reactors := make([]*MempoolReactor, N) +func makeAndConnectReactors(config *cfg.Config, N int) []*Reactor { + reactors := make([]*Reactor, N) logger := mempoolLogger() for i := 0; i < N; i++ { app := kvstore.NewKVStoreApplication() @@ -52,7 +51,7 @@ func makeAndConnectMempoolReactors(config *cfg.Config, N int) []*MempoolReactor mempool, cleanup := newMempoolWithApp(cc) defer cleanup() - reactors[i] = NewMempoolReactor(config.Mempool, mempool) // so we dont start the consensus states + reactors[i] = NewReactor(config.Mempool, mempool) // so we dont start the consensus states reactors[i].SetLogger(logger.With("validator", i)) } @@ -64,13 +63,15 @@ func makeAndConnectMempoolReactors(config *cfg.Config, N int) []*MempoolReactor return reactors } -// wait for all txs on all reactors -func waitForTxs(t *testing.T, txs types.Txs, reactors []*MempoolReactor) { +func waitForTxsOnReactors(t *testing.T, txs types.Txs, reactors []*Reactor) { // wait for the txs in all mempools wg := new(sync.WaitGroup) - for i := 0; i < len(reactors); i++ { + for i, reactor := range reactors { wg.Add(1) - go _waitForTxs(t, wg, txs, i, reactors) + go func(r *Reactor, reactorIndex int) { + defer wg.Done() + waitForTxsOnReactor(t, txs, r, reactorIndex) + }(reactor, i) } done := make(chan struct{}) @@ -87,25 +88,23 @@ func waitForTxs(t *testing.T, txs types.Txs, reactors []*MempoolReactor) { } } -// wait for all txs on a single mempool -func _waitForTxs(t *testing.T, wg *sync.WaitGroup, txs types.Txs, reactorIdx int, reactors []*MempoolReactor) { - - mempool := reactors[reactorIdx].Mempool - for mempool.Size() != len(txs) { +func waitForTxsOnReactor(t *testing.T, txs types.Txs, reactor *Reactor, reactorIndex int) { + mempool := reactor.mempool + for mempool.Size() < len(txs) { time.Sleep(time.Millisecond * 100) } reapedTxs := mempool.ReapMaxTxs(len(txs)) for i, tx := range txs { - assert.Equal(t, tx, reapedTxs[i], fmt.Sprintf("txs at index %d on reactor %d don't match: %v vs %v", i, reactorIdx, tx, reapedTxs[i])) + assert.Equalf(t, tx, reapedTxs[i], + "txs at index %d on reactor %d don't match: %v vs %v", i, reactorIndex, tx, reapedTxs[i]) } - wg.Done() } // ensure no txs on reactor after some timeout -func ensureNoTxs(t *testing.T, reactor *MempoolReactor, timeout time.Duration) { +func ensureNoTxs(t *testing.T, reactor *Reactor, timeout time.Duration) { time.Sleep(timeout) // wait for the txs in all mempools - assert.Zero(t, reactor.Mempool.Size()) + assert.Zero(t, reactor.mempool.Size()) } const ( @@ -116,7 +115,7 @@ const ( func TestReactorBroadcastTxMessage(t *testing.T) { config := cfg.TestConfig() const N = 4 - reactors := makeAndConnectMempoolReactors(config, N) + reactors := makeAndConnectReactors(config, N) defer func() { for _, r := range reactors { r.Stop() @@ -130,14 +129,14 @@ func TestReactorBroadcastTxMessage(t *testing.T) { // send a bunch of txs to the first reactor's mempool // and wait for them all to be received in the others - txs := checkTxs(t, reactors[0].Mempool, NUM_TXS, UnknownPeerID) - waitForTxs(t, txs, reactors) + txs := checkTxs(t, reactors[0].mempool, NUM_TXS, UnknownPeerID) + waitForTxsOnReactors(t, txs, reactors) } func TestReactorNoBroadcastToSender(t *testing.T) { config := cfg.TestConfig() const N = 2 - reactors := makeAndConnectMempoolReactors(config, N) + reactors := makeAndConnectReactors(config, N) defer func() { for _, r := range reactors { r.Stop() @@ -146,7 +145,7 @@ func TestReactorNoBroadcastToSender(t *testing.T) { // send a bunch of txs to the first reactor's mempool, claiming it came from peer // ensure peer gets no txs - checkTxs(t, reactors[0].Mempool, NUM_TXS, 1) + checkTxs(t, reactors[0].mempool, NUM_TXS, 1) ensureNoTxs(t, reactors[1], 100*time.Millisecond) } @@ -157,7 +156,7 @@ func TestBroadcastTxForPeerStopsWhenPeerStops(t *testing.T) { config := cfg.TestConfig() const N = 2 - reactors := makeAndConnectMempoolReactors(config, N) + reactors := makeAndConnectReactors(config, N) defer func() { for _, r := range reactors { r.Stop() @@ -180,7 +179,7 @@ func TestBroadcastTxForPeerStopsWhenReactorStops(t *testing.T) { config := cfg.TestConfig() const N = 2 - reactors := makeAndConnectMempoolReactors(config, N) + reactors := makeAndConnectReactors(config, N) // stop reactors for _, r := range reactors { diff --git a/mock/mempool.go b/mock/mempool.go new file mode 100644 index 000000000..66ad68d30 --- /dev/null +++ b/mock/mempool.go @@ -0,0 +1,45 @@ +package mock + +import ( + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/clist" + mempl "github.com/tendermint/tendermint/mempool" + "github.com/tendermint/tendermint/types" +) + +// Mempool is an empty implementation of a Mempool, useful for testing. +type Mempool struct{} + +var _ mempl.Mempool = Mempool{} + +func (Mempool) Lock() {} +func (Mempool) Unlock() {} +func (Mempool) Size() int { return 0 } +func (Mempool) CheckTx(_ types.Tx, _ func(*abci.Response)) error { + return nil +} +func (Mempool) CheckTxWithInfo(_ types.Tx, _ func(*abci.Response), + _ mempl.TxInfo) error { + return nil +} +func (Mempool) ReapMaxBytesMaxGas(_, _ int64) types.Txs { return types.Txs{} } +func (Mempool) ReapMaxTxs(n int) types.Txs { return types.Txs{} } +func (Mempool) Update( + _ int64, + _ types.Txs, + _ mempl.PreCheckFunc, + _ mempl.PostCheckFunc, +) error { + return nil +} +func (Mempool) Flush() {} +func (Mempool) FlushAppConn() error { return nil } +func (Mempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) } +func (Mempool) EnableTxsAvailable() {} +func (Mempool) TxsBytes() int64 { return 0 } + +func (Mempool) TxsFront() *clist.CElement { return nil } +func (Mempool) TxsWaitChan() <-chan struct{} { return nil } + +func (Mempool) InitWAL() {} +func (Mempool) CloseWAL() {} diff --git a/node/node.go b/node/node.go index 5af080ad1..1221e8b55 100644 --- a/node/node.go +++ b/node/node.go @@ -146,9 +146,10 @@ type Node struct { // services eventBus *types.EventBus // pub/sub for services stateDB dbm.DB - blockStore *bc.BlockStore // store the blockchain to disk - bcReactor *bc.BlockchainReactor // for fast-syncing - mempoolReactor *mempl.MempoolReactor // for gossipping transactions + blockStore *bc.BlockStore // store the blockchain to disk + bcReactor *bc.BlockchainReactor // for fast-syncing + mempoolReactor *mempl.Reactor // for gossipping transactions + mempool mempl.Mempool consensusState *cs.ConsensusState // latest consensus state consensusReactor *cs.ConsensusReactor // for participating in the consensus pexReactor *pex.PEXReactor // for exchanging peer addresses @@ -272,9 +273,9 @@ func onlyValidatorIsUs(state sm.State, privVal types.PrivValidator) bool { } func createMempoolAndMempoolReactor(config *cfg.Config, proxyApp proxy.AppConns, - state sm.State, memplMetrics *mempl.Metrics, logger log.Logger) (*mempl.MempoolReactor, *mempl.Mempool) { + state sm.State, memplMetrics *mempl.Metrics, logger log.Logger) (*mempl.Reactor, *mempl.CListMempool) { - mempool := mempl.NewMempool( + mempool := mempl.NewCListMempool( config.Mempool, proxyApp.Mempool(), state.LastBlockHeight, @@ -283,11 +284,7 @@ func createMempoolAndMempoolReactor(config *cfg.Config, proxyApp proxy.AppConns, mempl.WithPostCheck(sm.TxPostCheck(state)), ) mempoolLogger := logger.With("module", "mempool") - mempool.SetLogger(mempoolLogger) - if config.Mempool.WalEnabled() { - mempool.InitWAL() // no need to have the mempool wal during tests - } - mempoolReactor := mempl.NewMempoolReactor(config.Mempool, mempool) + mempoolReactor := mempl.NewReactor(config.Mempool, mempool) mempoolReactor.SetLogger(mempoolLogger) if config.Consensus.WaitForTxs() { @@ -315,7 +312,7 @@ func createConsensusReactor(config *cfg.Config, state sm.State, blockExec *sm.BlockExecutor, blockStore sm.BlockStore, - mempool *mempl.Mempool, + mempool *mempl.CListMempool, evidencePool *evidence.EvidencePool, privValidator types.PrivValidator, csMetrics *cs.Metrics, @@ -404,7 +401,7 @@ func createSwitch(config *cfg.Config, transport *p2p.MultiplexTransport, p2pMetrics *p2p.Metrics, peerFilters []p2p.PeerFilterFunc, - mempoolReactor *mempl.MempoolReactor, + mempoolReactor *mempl.Reactor, bcReactor *blockchain.BlockchainReactor, consensusReactor *consensus.ConsensusReactor, evidenceReactor *evidence.EvidenceReactor, @@ -622,6 +619,7 @@ func NewNode(config *cfg.Config, blockStore: blockStore, bcReactor: bcReactor, mempoolReactor: mempoolReactor, + mempool: mempool, consensusState: consensusState, consensusReactor: consensusReactor, pexReactor: pexReactor, @@ -673,6 +671,10 @@ func (n *Node) OnStart() error { n.isListening = true + if n.config.Mempool.WalEnabled() { + n.mempool.InitWAL() // no need to have the mempool wal during tests + } + // Start the switch (the P2P server). err = n.sw.Start() if err != nil { @@ -702,7 +704,7 @@ func (n *Node) OnStop() { // stop mempool WAL if n.config.Mempool.WalEnabled() { - n.mempoolReactor.Mempool.CloseWAL() + n.mempool.CloseWAL() } if err := n.transport.Close(); err != nil { @@ -737,7 +739,7 @@ func (n *Node) ConfigureRPC() { rpccore.SetStateDB(n.stateDB) rpccore.SetBlockStore(n.blockStore) rpccore.SetConsensusState(n.consensusState) - rpccore.SetMempool(n.mempoolReactor.Mempool) + rpccore.SetMempool(n.mempool) rpccore.SetEvidencePool(n.evidencePool) rpccore.SetP2PPeers(n.sw) rpccore.SetP2PTransport(n) @@ -884,11 +886,16 @@ func (n *Node) ConsensusReactor() *cs.ConsensusReactor { return n.consensusReactor } -// MempoolReactor returns the Node's MempoolReactor. -func (n *Node) MempoolReactor() *mempl.MempoolReactor { +// MempoolReactor returns the Node's mempool reactor. +func (n *Node) MempoolReactor() *mempl.Reactor { return n.mempoolReactor } +// Mempool returns the Node's mempool. +func (n *Node) Mempool() mempl.Mempool { + return n.mempool +} + // PEXReactor returns the Node's PEXReactor. It returns nil if PEX is disabled. func (n *Node) PEXReactor() *pex.PEXReactor { return n.pexReactor diff --git a/node/node_test.go b/node/node_test.go index a2725d845..6971ddd34 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -224,7 +224,7 @@ func TestCreateProposalBlock(t *testing.T) { // Make Mempool memplMetrics := mempl.PrometheusMetrics("node_test") - mempool := mempl.NewMempool( + mempool := mempl.NewCListMempool( config.Mempool, proxyApp.Mempool(), state.LastBlockHeight, diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index 1544a3d95..a1a48abc4 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -252,7 +252,8 @@ func TestAppCalls(t *testing.T) { func TestBroadcastTxSync(t *testing.T) { require := require.New(t) - mempool := node.MempoolReactor().Mempool + // TODO (melekes): use mempool which is set on RPC rather than getting it from node + mempool := node.Mempool() initMempoolSize := mempool.Size() for i, c := range GetClients() { @@ -272,7 +273,7 @@ func TestBroadcastTxSync(t *testing.T) { func TestBroadcastTxCommit(t *testing.T) { require := require.New(t) - mempool := node.MempoolReactor().Mempool + mempool := node.Mempool() for i, c := range GetClients() { _, _, tx := MakeTxKV() bres, err := c.BroadcastTxCommit(tx) @@ -287,7 +288,7 @@ func TestBroadcastTxCommit(t *testing.T) { func TestUnconfirmedTxs(t *testing.T) { _, _, tx := MakeTxKV() - mempool := node.MempoolReactor().Mempool + mempool := node.Mempool() _ = mempool.CheckTx(tx, nil) for i, c := range GetClients() { @@ -308,7 +309,7 @@ func TestUnconfirmedTxs(t *testing.T) { func TestNumUnconfirmedTxs(t *testing.T) { _, _, tx := MakeTxKV() - mempool := node.MempoolReactor().Mempool + mempool := node.Mempool() _ = mempool.CheckTx(tx, nil) mempoolSize := mempool.Size() diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index cefb0e371..28a492e6f 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -73,7 +73,7 @@ var ( txIndexer txindex.TxIndexer consensusReactor *consensus.ConsensusReactor eventBus *types.EventBus // thread safe - mempool *mempl.Mempool + mempool mempl.Mempool logger log.Logger @@ -88,7 +88,7 @@ func SetBlockStore(bs sm.BlockStore) { blockStore = bs } -func SetMempool(mem *mempl.Mempool) { +func SetMempool(mem mempl.Mempool) { mempool = mem } diff --git a/state/execution.go b/state/execution.go index 245325525..d872145ef 100644 --- a/state/execution.go +++ b/state/execution.go @@ -8,6 +8,7 @@ import ( dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/fail" "github.com/tendermint/tendermint/libs/log" + mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" ) @@ -30,7 +31,7 @@ type BlockExecutor struct { // manage the mempool lock during commit // and update both with block results after commit. - mempool Mempool + mempool mempl.Mempool evpool EvidencePool logger log.Logger @@ -48,7 +49,7 @@ func BlockExecutorWithMetrics(metrics *Metrics) BlockExecutorOption { // NewBlockExecutor returns a new BlockExecutor with a NopEventBus. // Call SetEventBus to provide one. -func NewBlockExecutor(db dbm.DB, logger log.Logger, proxyApp proxy.AppConnConsensus, mempool Mempool, evpool EvidencePool, options ...BlockExecutorOption) *BlockExecutor { +func NewBlockExecutor(db dbm.DB, logger log.Logger, proxyApp proxy.AppConnConsensus, mempool mempl.Mempool, evpool EvidencePool, options ...BlockExecutorOption) *BlockExecutor { res := &BlockExecutor{ db: db, proxyApp: proxyApp, diff --git a/state/execution_test.go b/state/execution_test.go index 092fbd68f..465b6b0be 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -16,10 +16,10 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" - tmtime "github.com/tendermint/tendermint/types/time" - + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" ) var ( @@ -38,7 +38,7 @@ func TestApplyBlock(t *testing.T) { state, stateDB := state(1, 1) blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), - MockMempool{}, MockEvidencePool{}) + mock.Mempool{}, MockEvidencePool{}) block := makeBlock(state, 1) blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} @@ -310,7 +310,7 @@ func TestEndBlockValidatorUpdates(t *testing.T) { state, stateDB := state(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), MockMempool{}, MockEvidencePool{}) + blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) eventBus := types.NewEventBus() err = eventBus.Start() @@ -367,7 +367,7 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { defer proxyApp.Stop() state, stateDB := state(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), MockMempool{}, MockEvidencePool{}) + blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) block := makeBlock(state, 1) blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} diff --git a/state/services.go b/state/services.go index 07d12c5a1..98f6afce3 100644 --- a/state/services.go +++ b/state/services.go @@ -1,8 +1,6 @@ package state import ( - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/types" ) @@ -11,57 +9,6 @@ import ( // NOTE: Interfaces used by RPC must be thread safe! //------------------------------------------------------ -//------------------------------------------------------ -// mempool - -// Mempool defines the mempool interface as used by the ConsensusState. -// Updates to the mempool need to be synchronized with committing a block -// so apps can reset their transient state on Commit -type Mempool interface { - Lock() - Unlock() - - Size() int - CheckTx(types.Tx, func(*abci.Response)) error - CheckTxWithInfo(types.Tx, func(*abci.Response), mempool.TxInfo) error - ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs - Update(int64, types.Txs, mempool.PreCheckFunc, mempool.PostCheckFunc) error - Flush() - FlushAppConn() error - - TxsAvailable() <-chan struct{} - EnableTxsAvailable() -} - -// MockMempool is an empty implementation of a Mempool, useful for testing. -type MockMempool struct{} - -var _ Mempool = MockMempool{} - -func (MockMempool) Lock() {} -func (MockMempool) Unlock() {} -func (MockMempool) Size() int { return 0 } -func (MockMempool) CheckTx(_ types.Tx, _ func(*abci.Response)) error { - return nil -} -func (MockMempool) CheckTxWithInfo(_ types.Tx, _ func(*abci.Response), - _ mempool.TxInfo) error { - return nil -} -func (MockMempool) ReapMaxBytesMaxGas(_, _ int64) types.Txs { return types.Txs{} } -func (MockMempool) Update( - _ int64, - _ types.Txs, - _ mempool.PreCheckFunc, - _ mempool.PostCheckFunc, -) error { - return nil -} -func (MockMempool) Flush() {} -func (MockMempool) FlushAppConn() error { return nil } -func (MockMempool) TxsAvailable() <-chan struct{} { return make(chan struct{}) } -func (MockMempool) EnableTxsAvailable() {} - //------------------------------------------------------ // blockstore @@ -96,7 +43,7 @@ type EvidencePool interface { IsCommitted(types.Evidence) bool } -// MockMempool is an empty implementation of a Mempool, useful for testing. +// MockEvidencePool is an empty implementation of a Mempool, useful for testing. type MockEvidencePool struct{} func (m MockEvidencePool) PendingEvidence(int64) []types.Evidence { return nil } From 4c60ab83c8b46339a30646e91a5c9a547356726e Mon Sep 17 00:00:00 2001 From: needkane Date: Sun, 5 May 2019 22:23:41 +0800 Subject: [PATCH 021/211] Update apps.md (#3621) --- docs/spec/abci/apps.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/spec/abci/apps.md b/docs/spec/abci/apps.md index 47e62eed9..908ad3eaf 100644 --- a/docs/spec/abci/apps.md +++ b/docs/spec/abci/apps.md @@ -265,7 +265,7 @@ This is enforced by Tendermint consensus. If a block includes evidence older than this, the block will be rejected (validators won't vote for it). -Must have `0 < MaxAge`. +Must have `MaxAge > 0`. ### Updates From 4905640e9bad30f89e74835924a024ef51783b02 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Sun, 5 May 2019 12:01:35 -0400 Subject: [PATCH 022/211] types: CommitVotes struct as last step towards #1648 (#3298) * VoteSignBytes builds CanonicalVote * CommitVotes implements VoteSetReader - new CommitVotes struct holds both the Commit and the ValidatorSet and implements VoteSetReader - ToVote takes a ValidatorSet * fix TestCommit * use CommitSig.BlockID Commits may include votes for a different BlockID, could be nil, or different altogether. This means we can't use `commit.BlockID` for reconstructing the sign bytes, since up to -1/3 of the commits might be for independent BlockIDs. This means CommitSig will need to include an indicator for what BlockID it signed - if it's not the committed one or nil, it will need to include it fully in order to be verified. This is unfortunate but unavoidable so long as we include votes for non-committed BlockIDs (which we do to track validator liveness) * fixes from review * remove CommitVotes. CommitSig contains address * remove commit.canonicalVote method * toVote -> getVote, takes valIdx * update adr-025 * commit.ToVoteSet -> CommitToVoteSet * add test * fix from review --- consensus/state.go | 11 +---- docs/architecture/adr-025-commit.md | 60 ++++++++++++++++++--------- types/block.go | 63 +++++++++++++++++++++++------ types/block_test.go | 24 +++++++++++ types/validator_set.go | 10 ++--- types/vote_set.go | 3 ++ 6 files changed, 124 insertions(+), 47 deletions(-) diff --git a/consensus/state.go b/consensus/state.go index a6256f82d..1f6bad9ab 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -484,16 +484,7 @@ func (cs *ConsensusState) reconstructLastCommit(state sm.State) { return } seenCommit := cs.blockStore.LoadSeenCommit(state.LastBlockHeight) - lastPrecommits := types.NewVoteSet(state.ChainID, state.LastBlockHeight, seenCommit.Round(), types.PrecommitType, state.LastValidators) - for _, precommit := range seenCommit.Precommits { - if precommit == nil { - continue - } - added, err := lastPrecommits.AddVote(seenCommit.ToVote(precommit)) - if !added || err != nil { - panic(fmt.Sprintf("Failed to reconstruct LastCommit: %v", err)) - } - } + lastPrecommits := types.CommitToVoteSet(state.ChainID, seenCommit, state.LastValidators) if !lastPrecommits.HasTwoThirdsMajority() { panic("Failed to reconstruct LastCommit: Does not have +2/3 maj") } diff --git a/docs/architecture/adr-025-commit.md b/docs/architecture/adr-025-commit.md index 3f2527951..6db039d43 100644 --- a/docs/architecture/adr-025-commit.md +++ b/docs/architecture/adr-025-commit.md @@ -1,14 +1,18 @@ # ADR 025 Commit ## Context + Currently the `Commit` structure contains a lot of potentially redundant or unnecessary data. -In particular it contains an array of every precommit from the validators, which includes many copies of the same data. Such as `Height`, `Round`, `Type`, and `BlockID`. Also the `ValidatorIndex` could be derived from the vote's position in the array, and the `ValidatorAddress` could potentially be derived from runtime context. The only truely necessary data is the `Signature` and `Timestamp` associated with each `Vote`. +It contains a list of precommits from every validator, where the precommit +includes the whole `Vote` structure. Thus each of the commit height, round, +type, and blockID are repeated for every validator, and could be deduplicated. ``` type Commit struct { BlockID BlockID `json:"block_id"` Precommits []*Vote `json:"precommits"` } + type Vote struct { ValidatorAddress Address `json:"validator_address"` ValidatorIndex int `json:"validator_index"` @@ -26,7 +30,9 @@ References: [#2226](https://github.com/tendermint/tendermint/issues/2226) ## Proposed Solution + We can improve efficiency by replacing the usage of the `Vote` struct with a subset of each vote, and by storing the constant values (`Height`, `Round`, `BlockID`) in the Commit itself. + ``` type Commit struct { Height int64 @@ -34,42 +40,56 @@ type Commit struct { BlockID BlockID `json:"block_id"` Precommits []*CommitSig `json:"precommits"` } + type CommitSig struct { + BlockID BlockIDFlag ValidatorAddress Address - Signature []byte Timestamp time.Time + Signature []byte } -``` -Continuing to store the `ValidatorAddress` in the `CommitSig` takes up extra space, but simplifies the process and allows for easier debugging. -## Status -Proposed -## Consequences +// indicate which BlockID the signature is for +type BlockIDFlag int -### Positive -The size of a `Commit` transmitted over the network goes from: +const ( + BlockIDFlagAbsent BlockIDFlag = iota // vote is not included in the Commit.Precommits + BlockIDFlagCommit // voted for the Commit.BlockID + BlockIDFlagNil // voted for nil +) -|BlockID| + n * (|Address| + |ValidatorIndex| + |Height| + |Round| + |Timestamp| + |Type| + |BlockID| + |Signature|) +``` -to: +Note the need for an extra byte to indicate whether the signature is for the BlockID or for nil. +This byte can also be used to indicate an absent vote, rather than using a nil object like we currently do, +which has been [problematic for compatibility between Amino and proto3](https://github.com/tendermint/go-amino/issues/260). +Note we also continue to store the `ValidatorAddress` in the `CommitSig`. +While this still takes 20-bytes per signature, it ensures that the Commit has all +information necessary to reconstruct Vote, which simplifies mapping between Commit and Vote objects +and with debugging. It also may be necessary for the light-client to know which address a signature corresponds to if +it is trying to verify a current commit with an older validtor set. + +## Status -|BlockID|+|Height|+|Round| + n*(|Address| + |Signature| + |Timestamp|) +Proposed -This saves: +## Consequences -n * (|BlockID| + |ValidatorIndex| + |Type|) + (n-1) * (Height + Round) +### Positive -In the current context, this would concretely be: -(assuming all ints are int64, and hashes are 32 bytes) +Removing the Type/Height/Round/Index and the BlockID saves roughly 80 bytes per precommit. +It varies because some integers are varint. The BlockID contains two 32-byte hashes an integer, +and the Height is 8-bytes. -n *(72 + 8 + 1 + 8 + 8) - 16 = n * 97 - 16 +For a chain with 100 validators, that's up to 8kB in savings per block! -With 100 validators this is a savings of almost 10KB on every block. ### Negative -This would add some complexity to the processing and verification of blocks and commits, as votes would have to be reconstructed to be verified and gossiped. The reconstruction could be relatively straightforward, only requiring the copying of data from the `Commit` itself into the newly created `Vote`. + +- Large breaking change to the block and commit structure +- Requires differentiating in code between the Vote and CommitSig objects, which may add some complexity (votes need to be reconstructed to be verified and gossiped) ### Neutral -This design leaves the `ValidatorAddress` in the `CommitSig` and in the `Vote`. These could be removed at some point for additional savings, but that would introduce more complexity, and make printing of `Commit` and `VoteSet` objects less informative, which could harm debugging efficiency and UI/UX. \ No newline at end of file + +- Commit.Precommits no longer contains nil values diff --git a/types/block.go b/types/block.go index 6616c0ee6..d793ce9dd 100644 --- a/types/block.go +++ b/types/block.go @@ -500,6 +500,8 @@ func (cs *CommitSig) toVote() *Vote { return &v } +//------------------------------------- + // Commit contains the evidence that a block was committed by a set of validators. // NOTE: Commit is empty for height 1, but never nil. type Commit struct { @@ -528,11 +530,56 @@ func NewCommit(blockID BlockID, precommits []*CommitSig) *Commit { } } +// Construct a VoteSet from the Commit and validator set. Panics +// if precommits from the commit can't be added to the voteset. +// Inverse of VoteSet.MakeCommit(). +func CommitToVoteSet(chainID string, commit *Commit, vals *ValidatorSet) *VoteSet { + height, round, typ := commit.Height(), commit.Round(), PrecommitType + voteSet := NewVoteSet(chainID, height, round, typ, vals) + for idx, precommit := range commit.Precommits { + if precommit == nil { + continue + } + added, err := voteSet.AddVote(commit.GetVote(idx)) + if !added || err != nil { + panic(fmt.Sprintf("Failed to reconstruct LastCommit: %v", err)) + } + } + return voteSet +} + +// GetVote converts the CommitSig for the given valIdx to a Vote. +// Returns nil if the precommit at valIdx is nil. +// Panics if valIdx >= commit.Size(). +func (commit *Commit) GetVote(valIdx int) *Vote { + commitSig := commit.Precommits[valIdx] + if commitSig == nil { + return nil + } + + // NOTE: this commitSig might be for a nil blockID, + // so we can't just use commit.BlockID here. + // For #1648, CommitSig will need to indicate what BlockID it's for ! + blockID := commitSig.BlockID + commit.memoizeHeightRound() + return &Vote{ + Type: PrecommitType, + Height: commit.height, + Round: commit.round, + BlockID: blockID, + Timestamp: commitSig.Timestamp, + ValidatorAddress: commitSig.ValidatorAddress, + ValidatorIndex: valIdx, + Signature: commitSig.Signature, + } +} + // VoteSignBytes constructs the SignBytes for the given CommitSig. // The only unique part of the SignBytes is the Timestamp - all other fields // signed over are otherwise the same for all validators. -func (commit *Commit) VoteSignBytes(chainID string, cs *CommitSig) []byte { - return commit.ToVote(cs).SignBytes(chainID) +// Panics if valIdx >= commit.Size(). +func (commit *Commit) VoteSignBytes(chainID string, valIdx int) []byte { + return commit.GetVote(valIdx).SignBytes(chainID) } // memoizeHeightRound memoizes the height and round of the commit using @@ -553,14 +600,6 @@ func (commit *Commit) memoizeHeightRound() { } } -// ToVote converts a CommitSig to a Vote. -// If the CommitSig is nil, the Vote will be nil. -func (commit *Commit) ToVote(cs *CommitSig) *Vote { - // TODO: use commit.validatorSet to reconstruct vote - // and deprecate .toVote - return cs.toVote() -} - // Height returns the height of the commit func (commit *Commit) Height() int64 { commit.memoizeHeightRound() @@ -602,8 +641,8 @@ func (commit *Commit) BitArray() *cmn.BitArray { // GetByIndex returns the vote corresponding to a given validator index. // Panics if `index >= commit.Size()`. // Implements VoteSetReader. -func (commit *Commit) GetByIndex(index int) *Vote { - return commit.ToVote(commit.Precommits[index]) +func (commit *Commit) GetByIndex(valIdx int) *Vote { + return commit.GetVote(valIdx) } // IsCommit returns true if there is at least one vote. diff --git a/types/block_test.go b/types/block_test.go index 75b5c19dd..ff7edd27a 100644 --- a/types/block_test.go +++ b/types/block_test.go @@ -342,3 +342,27 @@ func TestBlockMaxDataBytesUnknownEvidence(t *testing.T) { } } } + +func TestCommitToVoteSet(t *testing.T) { + lastID := makeBlockIDRandom() + h := int64(3) + + voteSet, valSet, vals := randVoteSet(h-1, 1, PrecommitType, 10, 1) + commit, err := MakeCommit(lastID, h-1, 1, voteSet, vals) + assert.NoError(t, err) + + chainID := voteSet.ChainID() + voteSet2 := CommitToVoteSet(chainID, commit, valSet) + + for i := 0; i < len(vals); i++ { + vote1 := voteSet.GetByIndex(i) + vote2 := voteSet2.GetByIndex(i) + vote3 := commit.GetVote(i) + + vote1bz := cdc.MustMarshalBinaryBare(vote1) + vote2bz := cdc.MustMarshalBinaryBare(vote2) + vote3bz := cdc.MustMarshalBinaryBare(vote3) + assert.Equal(t, vote1bz, vote2bz) + assert.Equal(t, vote1bz, vote3bz) + } +} diff --git a/types/validator_set.go b/types/validator_set.go index 36ce67f06..9e78fbc77 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -612,7 +612,7 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i } _, val := vals.GetByIndex(idx) // Validate signature. - precommitSignBytes := commit.VoteSignBytes(chainID, precommit) + precommitSignBytes := commit.VoteSignBytes(chainID, idx) if !val.PubKey.VerifyBytes(precommitSignBytes, precommit.Signature) { return fmt.Errorf("Invalid commit -- invalid signature: %v", precommit) } @@ -689,14 +689,14 @@ func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID strin return cmn.NewError("Invalid commit -- not precommit @ index %v", idx) } // See if this validator is in oldVals. - idx, val := oldVals.GetByAddress(precommit.ValidatorAddress) - if val == nil || seen[idx] { + oldIdx, val := oldVals.GetByAddress(precommit.ValidatorAddress) + if val == nil || seen[oldIdx] { continue // missing or double vote... } - seen[idx] = true + seen[oldIdx] = true // Validate signature. - precommitSignBytes := commit.VoteSignBytes(chainID, precommit) + precommitSignBytes := commit.VoteSignBytes(chainID, idx) if !val.PubKey.VerifyBytes(precommitSignBytes, precommit.Signature) { return cmn.NewError("Invalid commit -- invalid signature: %v", precommit) } diff --git a/types/vote_set.go b/types/vote_set.go index 2c896b39c..a4a42bb4c 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -528,6 +528,9 @@ func (voteSet *VoteSet) sumTotalFrac() (int64, int64, float64) { //-------------------------------------------------------------------------------- // Commit +// MakeCommit constructs a Commit from the VoteSet. +// Panics if the vote type is not PrecommitType or if +// there's no +2/3 votes for a single block. func (voteSet *VoteSet) MakeCommit() *Commit { if voteSet.type_ != PrecommitType { panic("Cannot MakeCommit() unless VoteSet.Type is PrecommitType") From e2f547154592ebe3f174a15b52639d691dc53fbc Mon Sep 17 00:00:00 2001 From: Daniil Lashin Date: Sun, 5 May 2019 19:19:19 +0300 Subject: [PATCH 023/211] ADR-037: DeliverBlock (#3420) * ADR 037 Initial draft * Add link to initial conversation * Update initial draft * Fix indentations * Change negative consequences * Fix indentations * Change ADR id * Add one more positive consequence * Rollback ADR number --- docs/architecture/adr-038-deliver-block.md | 100 +++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 docs/architecture/adr-038-deliver-block.md diff --git a/docs/architecture/adr-038-deliver-block.md b/docs/architecture/adr-038-deliver-block.md new file mode 100644 index 000000000..ac23ab693 --- /dev/null +++ b/docs/architecture/adr-038-deliver-block.md @@ -0,0 +1,100 @@ +# ADR 037: Deliver Block + +Author: Daniil Lashin (@danil-lashin) + +## Changelog + +13-03-2019: Initial draft + +## Context + +Initial conversation: https://github.com/tendermint/tendermint/issues/2901 + +Some applications can handle transactions in parallel, or at least some +part of tx processing can be parallelized. Now it is not possible for developer +to execute txs in parallel because Tendermint delivers them consequentially. + +## Decision + +Now Tendermint have `BeginBlock`, `EndBlock`, `Commit`, `DeliverTx` steps +while executing block. This doc proposes merging this steps into one `DeliverBlock` +step. It will allow developers of applications to decide how they want to +execute transactions (in parallel or consequentially). Also it will simplify and +speed up communications between application and Tendermint. + +As @jaekwon [mentioned](https://github.com/tendermint/tendermint/issues/2901#issuecomment-477746128) +in discussion not all application will benefit from this solution. In some cases, +when application handles transaction consequentially, it way slow down the blockchain, +because it need to wait until full block is transmitted to application to start +processing it. Also, in the case of complete change of ABCI, we need to force all the apps +to change their implementation completely. That's why I propose to introduce one more ABCI +type. + +# Implementation Changes + +In addition to default application interface which now have this structure + +```go +type Application interface { + // Info and Mempool methods... + + // Consensus Connection + InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore + BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block + DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing + EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set + Commit() ResponseCommit // Commit the state and return the application Merkle root hash +} +``` + +this doc proposes to add one more: + +```go +type Application interface { + // Info and Mempool methods... + + // Consensus Connection + InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore + DeliverBlock(RequestDeliverBlock) ResponseDeliverBlock // Deliver full block + Commit() ResponseCommit // Commit the state and return the application Merkle root hash +} + +type RequestDeliverBlock struct { + Hash []byte + Header Header + Txs Txs + LastCommitInfo LastCommitInfo + ByzantineValidators []Evidence +} + +type ResponseDeliverBlock struct { + ValidatorUpdates []ValidatorUpdate + ConsensusParamUpdates *ConsensusParams + Tags []common.KVPair + TxResults []ResponseDeliverTx +} + +``` + +Also, we will need to add new config param, which will specify what kind of ABCI application uses. +For example, it can be `abci_type`. Then we will have 2 types: +- `advanced` - current ABCI +- `simple` - proposed implementation + +## Status + +In review + +## Consequences + +### Positive + +- much simpler introduction and tutorials for new developers (instead of implementing 5 methods whey +will need to implement only 3) +- txs can be handled in parallel +- simpler interface +- faster communications between Tendermint and application + +### Negative + +- Tendermint should now support 2 kinds of ABCI From 86f2765b32ab92864e1a59eab08663b06904d518 Mon Sep 17 00:00:00 2001 From: Mostafa Sedaghat Joo Date: Mon, 6 May 2019 00:19:41 +0800 Subject: [PATCH 024/211] Add adr-36 (#3324) --- .../adr-036-validator-set-by-app.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 docs/architecture/adr-036-validator-set-by-app.md diff --git a/docs/architecture/adr-036-validator-set-by-app.md b/docs/architecture/adr-036-validator-set-by-app.md new file mode 100644 index 000000000..ff752b75d --- /dev/null +++ b/docs/architecture/adr-036-validator-set-by-app.md @@ -0,0 +1,29 @@ +# ADR 036: Application should be in charge of validator set + +## Changelog + + +## Context + +Currently Tendermint is in charge of validator set and proposer selection. Application can only update the validator set changes at EndBlock time. +To support Light Client, application should make sure at least 2/3 of validator are same at each round. + +Application should have full control on validator set changes and proposer selection. In each round Application can provide the list of validators for next rounds in order with their power. The proposer is the first in the list, in case the proposer is offline, the next one can propose the proposal and so on. + +## Decision + +## Status + +## Consequences + +Tendermint is no more in charge of validator set and its changes. The Application should provide the correct information. +However Tendermint can provide psedo-randomness algorithm to help application for selecting proposer in each round. + +### Positive + +### Negative + +### Neutral + +## References + From 4519ef3109fccebadf3d691e7d0c1ee3217636be Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Sun, 5 May 2019 12:42:26 -0400 Subject: [PATCH 025/211] adr renames (#3622) --- ...iver-block.md => adr-037-deliver-block.md} | 32 +++++++++---------- ...=> adr-041-proposer-selection-via-abci.md} | 2 +- 2 files changed, 17 insertions(+), 17 deletions(-) rename docs/architecture/{adr-038-deliver-block.md => adr-037-deliver-block.md} (87%) rename docs/architecture/{adr-036-validator-set-by-app.md => adr-041-proposer-selection-via-abci.md} (93%) diff --git a/docs/architecture/adr-038-deliver-block.md b/docs/architecture/adr-037-deliver-block.md similarity index 87% rename from docs/architecture/adr-038-deliver-block.md rename to docs/architecture/adr-037-deliver-block.md index ac23ab693..31907c9ae 100644 --- a/docs/architecture/adr-038-deliver-block.md +++ b/docs/architecture/adr-037-deliver-block.md @@ -10,24 +10,24 @@ Author: Daniil Lashin (@danil-lashin) Initial conversation: https://github.com/tendermint/tendermint/issues/2901 -Some applications can handle transactions in parallel, or at least some +Some applications can handle transactions in parallel, or at least some part of tx processing can be parallelized. Now it is not possible for developer to execute txs in parallel because Tendermint delivers them consequentially. ## Decision -Now Tendermint have `BeginBlock`, `EndBlock`, `Commit`, `DeliverTx` steps -while executing block. This doc proposes merging this steps into one `DeliverBlock` -step. It will allow developers of applications to decide how they want to -execute transactions (in parallel or consequentially). Also it will simplify and -speed up communications between application and Tendermint. +Now Tendermint have `BeginBlock`, `EndBlock`, `Commit`, `DeliverTx` steps +while executing block. This doc proposes merging this steps into one `DeliverBlock` +step. It will allow developers of applications to decide how they want to +execute transactions (in parallel or consequentially). Also it will simplify and +speed up communications between application and Tendermint. -As @jaekwon [mentioned](https://github.com/tendermint/tendermint/issues/2901#issuecomment-477746128) -in discussion not all application will benefit from this solution. In some cases, +As @jaekwon [mentioned](https://github.com/tendermint/tendermint/issues/2901#issuecomment-477746128) +in discussion not all application will benefit from this solution. In some cases, when application handles transaction consequentially, it way slow down the blockchain, -because it need to wait until full block is transmitted to application to start +because it need to wait until full block is transmitted to application to start processing it. Also, in the case of complete change of ABCI, we need to force all the apps -to change their implementation completely. That's why I propose to introduce one more ABCI +to change their implementation completely. That's why I propose to introduce one more ABCI type. # Implementation Changes @@ -52,7 +52,7 @@ this doc proposes to add one more: ```go type Application interface { // Info and Mempool methods... - + // Consensus Connection InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore DeliverBlock(RequestDeliverBlock) ResponseDeliverBlock // Deliver full block @@ -61,9 +61,9 @@ type Application interface { type RequestDeliverBlock struct { Hash []byte - Header Header + Header Header Txs Txs - LastCommitInfo LastCommitInfo + LastCommitInfo LastCommitInfo ByzantineValidators []Evidence } @@ -71,7 +71,7 @@ type ResponseDeliverBlock struct { ValidatorUpdates []ValidatorUpdate ConsensusParamUpdates *ConsensusParams Tags []common.KVPair - TxResults []ResponseDeliverTx + TxResults []ResponseDeliverTx } ``` @@ -79,7 +79,7 @@ type ResponseDeliverBlock struct { Also, we will need to add new config param, which will specify what kind of ABCI application uses. For example, it can be `abci_type`. Then we will have 2 types: - `advanced` - current ABCI -- `simple` - proposed implementation +- `simple` - proposed implementation ## Status @@ -89,7 +89,7 @@ In review ### Positive -- much simpler introduction and tutorials for new developers (instead of implementing 5 methods whey +- much simpler introduction and tutorials for new developers (instead of implementing 5 methods whey will need to implement only 3) - txs can be handled in parallel - simpler interface diff --git a/docs/architecture/adr-036-validator-set-by-app.md b/docs/architecture/adr-041-proposer-selection-via-abci.md similarity index 93% rename from docs/architecture/adr-036-validator-set-by-app.md rename to docs/architecture/adr-041-proposer-selection-via-abci.md index ff752b75d..58bf20de3 100644 --- a/docs/architecture/adr-036-validator-set-by-app.md +++ b/docs/architecture/adr-041-proposer-selection-via-abci.md @@ -1,4 +1,4 @@ -# ADR 036: Application should be in charge of validator set +# ADR 041: Application should be in charge of validator set ## Changelog From debf8f70c9026157c433c952f7893c0b1e52574f Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 6 May 2019 13:42:21 +0400 Subject: [PATCH 026/211] libs/db: bbolt (etcd's fork of bolt) (#3610) Description: add boltdb implement for db interface. The boltdb is safe and high performence embedded KV database. It is based on B+tree and Mmap. Now it is maintained by etcd-io org. https://github.com/etcd-io/bbolt It is much better than LSM tree (levelDB) when RANDOM and SCAN read. Replaced #3604 Refs #1835 Commits: * add boltdb for storage * update boltdb in gopkg.toml * update bbolt in Gopkg.lock * dep update Gopkg.lock * add boltdb'bucket when create Boltdb * some modifies * address my own comments * fix most of the Iterator tests for boltdb * bolt does not support concurrent writes while iterating * add WARNING word * note that ReadOnly option is not supported * fixes after Ismail's review Co-Authored-By: melekes * panic if View returns error plus specify bucket in the top comment --- Gopkg.lock | 17 +- Gopkg.toml | 4 + libs/db/boltdb.go | 323 ++++++++++++++++++++++++++++++++++++ libs/db/boltdb_test.go | 35 ++++ libs/db/common_test.go | 66 ++++++++ libs/db/db.go | 1 + libs/db/fsdb.go | 2 +- libs/db/go_level_db_test.go | 95 ++--------- libs/db/mem_db.go | 2 +- libs/db/util_test.go | 5 + 10 files changed, 468 insertions(+), 82 deletions(-) create mode 100644 libs/db/boltdb.go create mode 100644 libs/db/boltdb_test.go diff --git a/Gopkg.lock b/Gopkg.lock index 530cd89dd..6d9236559 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -34,6 +34,14 @@ revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" version = "v1.1.1" +[[projects]] + digest = "1:5f7414cf41466d4b4dd7ec52b2cd3e481e08cfd11e7e24fef730c0e483e88bb1" + name = "github.com/etcd-io/bbolt" + packages = ["."] + pruneopts = "UT" + revision = "63597a96ec0ad9e6d43c3fc81e809909e0237461" + version = "v1.3.2" + [[projects]] digest = "1:544229a3ca0fb2dd5ebc2896d3d2ff7ce096d9751635301e44e37e761349ee70" name = "github.com/fortytw2/leaktest" @@ -170,9 +178,12 @@ revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" [[projects]] - digest = "1:c568d7727aa262c32bdf8a3f7db83614f7af0ed661474b24588de635c20024c7" + digest = "1:53e8c5c79716437e601696140e8b1801aae4204f4ec54a504333702a49572c4f" name = "github.com/magiconair/properties" - packages = ["."] + packages = [ + ".", + "assert", + ] pruneopts = "UT" revision = "c2353362d570a7bfa228149c62842019201cfb71" version = "v1.8.0" @@ -500,6 +511,7 @@ "github.com/btcsuite/btcd/btcec", "github.com/btcsuite/btcutil/base58", "github.com/btcsuite/btcutil/bech32", + "github.com/etcd-io/bbolt", "github.com/fortytw2/leaktest", "github.com/go-kit/kit/log", "github.com/go-kit/kit/log/level", @@ -516,6 +528,7 @@ "github.com/golang/protobuf/ptypes/timestamp", "github.com/gorilla/websocket", "github.com/jmhodges/levigo", + "github.com/magiconair/properties/assert", "github.com/pkg/errors", "github.com/prometheus/client_golang/prometheus", "github.com/prometheus/client_golang/prometheus/promhttp", diff --git a/Gopkg.toml b/Gopkg.toml index 505f0da4a..e5abf051c 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -22,6 +22,10 @@ ########################################################### # Allow only patch releases for serialization libraries +[[constraint]] + name = "github.com/etcd-io/bbolt" + version = "v1.3.2" + [[constraint]] name = "github.com/tendermint/go-amino" version = "~0.14.1" diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go new file mode 100644 index 000000000..6a31f11a9 --- /dev/null +++ b/libs/db/boltdb.go @@ -0,0 +1,323 @@ +package db + +import ( + "bytes" + "errors" + "fmt" + "os" + "path/filepath" + "sync" + + "github.com/etcd-io/bbolt" +) + +var bucket = []byte("tm") + +func init() { + registerDBCreator(BoltDBBackend, func(name, dir string) (DB, error) { + return NewBoltDB(name, dir) + }, false) +} + +// BoltDB is a wrapper around etcd's fork of bolt +// (https://github.com/etcd-io/bbolt). +// +// NOTE: All operations (including Set, Delete) are synchronous by default. One +// can globally turn it off by using NoSync config option (not recommended). +// +// A single bucket ([]byte("tm")) is used per a database instance. This could +// lead to performance issues when/if there will be lots of keys. +type BoltDB struct { + db *bbolt.DB +} + +// NewBoltDB returns a BoltDB with default options. +func NewBoltDB(name, dir string) (DB, error) { + return NewBoltDBWithOpts(name, dir, bbolt.DefaultOptions) +} + +// NewBoltDBWithOpts allows you to supply *bbolt.Options. ReadOnly: true is not +// supported because NewBoltDBWithOpts creates a global bucket. +func NewBoltDBWithOpts(name string, dir string, opts *bbolt.Options) (DB, error) { + if opts.ReadOnly { + return nil, errors.New("ReadOnly: true is not supported") + } + + dbPath := filepath.Join(dir, name+".db") + db, err := bbolt.Open(dbPath, os.ModePerm, opts) + if err != nil { + return nil, err + } + + // create a global bucket + err = db.Update(func(tx *bbolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(bucket) + return err + }) + if err != nil { + return nil, err + } + + return &BoltDB{db: db}, nil +} + +func (bdb *BoltDB) Get(key []byte) (value []byte) { + key = nonEmptyKey(nonNilBytes(key)) + err := bdb.db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(bucket) + value = b.Get(key) + return nil + }) + if err != nil { + panic(err) + } + return +} + +func (bdb *BoltDB) Has(key []byte) bool { + return bdb.Get(key) != nil +} + +func (bdb *BoltDB) Set(key, value []byte) { + key = nonEmptyKey(nonNilBytes(key)) + value = nonNilBytes(value) + err := bdb.db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(bucket) + return b.Put(key, value) + }) + if err != nil { + panic(err) + } +} + +func (bdb *BoltDB) SetSync(key, value []byte) { + bdb.Set(key, value) +} + +func (bdb *BoltDB) Delete(key []byte) { + key = nonEmptyKey(nonNilBytes(key)) + err := bdb.db.Update(func(tx *bbolt.Tx) error { + return tx.Bucket(bucket).Delete(key) + }) + if err != nil { + panic(err) + } +} + +func (bdb *BoltDB) DeleteSync(key []byte) { + bdb.Delete(key) +} + +func (bdb *BoltDB) Close() { + bdb.db.Close() +} + +func (bdb *BoltDB) Print() { + stats := bdb.db.Stats() + fmt.Printf("%v\n", stats) + + err := bdb.db.View(func(tx *bbolt.Tx) error { + tx.Bucket(bucket).ForEach(func(k, v []byte) error { + fmt.Printf("[%X]:\t[%X]\n", k, v) + return nil + }) + return nil + }) + if err != nil { + panic(err) + } +} + +func (bdb *BoltDB) Stats() map[string]string { + stats := bdb.db.Stats() + m := make(map[string]string) + + // Freelist stats + m["FreePageN"] = fmt.Sprintf("%v", stats.FreePageN) + m["PendingPageN"] = fmt.Sprintf("%v", stats.PendingPageN) + m["FreeAlloc"] = fmt.Sprintf("%v", stats.FreeAlloc) + m["FreelistInuse"] = fmt.Sprintf("%v", stats.FreelistInuse) + + // Transaction stats + m["TxN"] = fmt.Sprintf("%v", stats.TxN) + m["OpenTxN"] = fmt.Sprintf("%v", stats.OpenTxN) + + return m +} + +// boltDBBatch stores key values in sync.Map and dumps them to the underlying +// DB upon Write call. +type boltDBBatch struct { + buffer *sync.Map + db *BoltDB +} + +// NewBatch returns a new batch. +func (bdb *BoltDB) NewBatch() Batch { + return &boltDBBatch{ + buffer: &sync.Map{}, + db: bdb, + } +} + +func (bdb *boltDBBatch) Set(key, value []byte) { + bdb.buffer.Store(key, value) +} + +func (bdb *boltDBBatch) Delete(key []byte) { + bdb.buffer.Delete(key) +} + +// NOTE: the operation is synchronous (see BoltDB for reasons) +func (bdb *boltDBBatch) Write() { + err := bdb.db.db.Batch(func(tx *bbolt.Tx) error { + b := tx.Bucket(bucket) + var putErr error + bdb.buffer.Range(func(key, value interface{}) bool { + putErr = b.Put(key.([]byte), value.([]byte)) + return putErr == nil // stop if putErr is not nil + }) + return putErr + }) + if err != nil { + panic(err) + } +} + +func (bdb *boltDBBatch) WriteSync() { + bdb.Write() +} + +func (bdb *boltDBBatch) Close() {} + +// WARNING: Any concurrent writes (Set, SetSync) will block until the Iterator +// is closed. +func (bdb *BoltDB) Iterator(start, end []byte) Iterator { + tx, err := bdb.db.Begin(false) + if err != nil { + panic(err) + } + c := tx.Bucket(bucket).Cursor() + return newBoltDBIterator(c, start, end, false) +} + +// WARNING: Any concurrent writes (Set, SetSync) will block until the Iterator +// is closed. +func (bdb *BoltDB) ReverseIterator(start, end []byte) Iterator { + tx, err := bdb.db.Begin(false) + if err != nil { + panic(err) + } + c := tx.Bucket(bucket).Cursor() + return newBoltDBIterator(c, start, end, true) +} + +// boltDBIterator allows you to iterate on range of keys/values given some +// start / end keys (nil & nil will result in doing full scan). +type boltDBIterator struct { + itr *bbolt.Cursor + start []byte + end []byte + + currentKey []byte + currentValue []byte + + isInvalid bool + isReverse bool +} + +func newBoltDBIterator(itr *bbolt.Cursor, start, end []byte, isReverse bool) *boltDBIterator { + var ck, cv []byte + if isReverse { + if end == nil { + ck, cv = itr.Last() + } else { + _, _ = itr.Seek(end) // after key + ck, cv = itr.Prev() // return to end key + } + } else { + if start == nil { + ck, cv = itr.First() + } else { + ck, cv = itr.Seek(start) + } + } + + return &boltDBIterator{ + itr: itr, + start: start, + end: end, + currentKey: ck, + currentValue: cv, + isReverse: isReverse, + isInvalid: false, + } +} + +func (itr *boltDBIterator) Domain() ([]byte, []byte) { + return itr.start, itr.end +} + +func (itr *boltDBIterator) Valid() bool { + if itr.isInvalid { + return false + } + + // iterated to the end of the cursor + if len(itr.currentKey) == 0 { + itr.isInvalid = true + return false + } + + if itr.isReverse { + if itr.start != nil && bytes.Compare(itr.currentKey, itr.start) < 0 { + itr.isInvalid = true + return false + } + } else { + if itr.end != nil && bytes.Compare(itr.end, itr.currentKey) <= 0 { + itr.isInvalid = true + return false + } + } + + // Valid + return true +} + +func (itr *boltDBIterator) Next() { + itr.assertIsValid() + if itr.isReverse { + itr.currentKey, itr.currentValue = itr.itr.Prev() + } else { + itr.currentKey, itr.currentValue = itr.itr.Next() + } +} + +func (itr *boltDBIterator) Key() []byte { + itr.assertIsValid() + return itr.currentKey +} + +func (itr *boltDBIterator) Value() []byte { + itr.assertIsValid() + return itr.currentValue +} + +// boltdb cursor has no close op. +func (itr *boltDBIterator) Close() {} + +func (itr *boltDBIterator) assertIsValid() { + if !itr.Valid() { + panic("Boltdb-iterator is invalid") + } +} + +// nonEmptyKey returns a []byte("nil") if key is empty. +// WARNING: this may collude with "nil" user key! +func nonEmptyKey(key []byte) []byte { + if len(key) == 0 { + return []byte("nil") + } + return key +} diff --git a/libs/db/boltdb_test.go b/libs/db/boltdb_test.go new file mode 100644 index 000000000..ced57d2e0 --- /dev/null +++ b/libs/db/boltdb_test.go @@ -0,0 +1,35 @@ +package db + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/require" + + cmn "github.com/tendermint/tendermint/libs/common" +) + +func TestBoltDBNewBoltDB(t *testing.T) { + name := fmt.Sprintf("test_%x", cmn.RandStr(12)) + dir := os.TempDir() + defer cleanupDBDir(dir, name) + + db, err := NewBoltDB(name, dir) + require.NoError(t, err) + db.Close() +} + +func BenchmarkBoltDBRandomReadsWrites(b *testing.B) { + name := fmt.Sprintf("test_%x", cmn.RandStr(12)) + db, err := NewBoltDB(name, "") + if err != nil { + b.Fatal(err) + } + defer func() { + db.Close() + cleanupDBDir("", name) + }() + + benchmarkRandomReadsWrites(b, db) +} diff --git a/libs/db/common_test.go b/libs/db/common_test.go index 1e27a7cac..64a86979c 100644 --- a/libs/db/common_test.go +++ b/libs/db/common_test.go @@ -1,6 +1,8 @@ package db import ( + "bytes" + "encoding/binary" "fmt" "io/ioutil" "sync" @@ -8,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + cmn "github.com/tendermint/tendermint/libs/common" ) //---------------------------------------- @@ -188,3 +191,66 @@ func (mockIterator) Value() []byte { func (mockIterator) Close() { } + +func benchmarkRandomReadsWrites(b *testing.B, db DB) { + b.StopTimer() + + // create dummy data + const numItems = int64(1000000) + internal := map[int64]int64{} + for i := 0; i < int(numItems); i++ { + internal[int64(i)] = int64(0) + } + + // fmt.Println("ok, starting") + b.StartTimer() + + for i := 0; i < b.N; i++ { + // Write something + { + idx := int64(cmn.RandInt()) % numItems + internal[idx]++ + val := internal[idx] + idxBytes := int642Bytes(int64(idx)) + valBytes := int642Bytes(int64(val)) + //fmt.Printf("Set %X -> %X\n", idxBytes, valBytes) + db.Set(idxBytes, valBytes) + } + + // Read something + { + idx := int64(cmn.RandInt()) % numItems + valExp := internal[idx] + idxBytes := int642Bytes(int64(idx)) + valBytes := db.Get(idxBytes) + //fmt.Printf("Get %X -> %X\n", idxBytes, valBytes) + if valExp == 0 { + if !bytes.Equal(valBytes, nil) { + b.Errorf("Expected %v for %v, got %X", nil, idx, valBytes) + break + } + } else { + if len(valBytes) != 8 { + b.Errorf("Expected length 8 for %v, got %X", idx, valBytes) + break + } + valGot := bytes2Int64(valBytes) + if valExp != valGot { + b.Errorf("Expected %v for %v, got %v", valExp, idx, valGot) + break + } + } + } + + } +} + +func int642Bytes(i int64) []byte { + buf := make([]byte, 8) + binary.BigEndian.PutUint64(buf, uint64(i)) + return buf +} + +func bytes2Int64(buf []byte) int64 { + return int64(binary.BigEndian.Uint64(buf)) +} diff --git a/libs/db/db.go b/libs/db/db.go index 8a3975a84..43b788281 100644 --- a/libs/db/db.go +++ b/libs/db/db.go @@ -16,6 +16,7 @@ const ( GoLevelDBBackend DBBackendType = "goleveldb" MemDBBackend DBBackendType = "memdb" FSDBBackend DBBackendType = "fsdb" // using the filesystem naively + BoltDBBackend DBBackendType = "boltdb" ) type dbCreator func(name string, dir string) (DB, error) diff --git a/libs/db/fsdb.go b/libs/db/fsdb.go index 2d82e7749..ca8eefe94 100644 --- a/libs/db/fsdb.go +++ b/libs/db/fsdb.go @@ -20,7 +20,7 @@ const ( ) func init() { - registerDBCreator(FSDBBackend, func(name string, dir string) (DB, error) { + registerDBCreator(FSDBBackend, func(name, dir string) (DB, error) { dbPath := filepath.Join(dir, name+".db") return NewFSDB(dbPath), nil }, false) diff --git a/libs/db/go_level_db_test.go b/libs/db/go_level_db_test.go index c24eec3c8..f781a2b3d 100644 --- a/libs/db/go_level_db_test.go +++ b/libs/db/go_level_db_test.go @@ -1,29 +1,27 @@ package db import ( - "bytes" - "encoding/binary" "fmt" - "os" "testing" + "github.com/stretchr/testify/require" "github.com/syndtr/goleveldb/leveldb/opt" - "github.com/stretchr/testify/require" cmn "github.com/tendermint/tendermint/libs/common" ) -func TestNewGoLevelDB(t *testing.T) { +func TestGoLevelDBNewGoLevelDB(t *testing.T) { name := fmt.Sprintf("test_%x", cmn.RandStr(12)) - // Test write locks - db, err := NewGoLevelDB(name, "") + defer cleanupDBDir("", name) + + // Test we can't open the db twice for writing + wr1, err := NewGoLevelDB(name, "") require.Nil(t, err) - defer os.RemoveAll("./" + name + ".db") _, err = NewGoLevelDB(name, "") require.NotNil(t, err) - db.Close() // Close the db to release the lock + wr1.Close() // Close the db to release the lock - // Open the db twice in a row to test read-only locks + // Test we can open the db twice for reading only ro1, err := NewGoLevelDBWithOpts(name, "", &opt.Options{ReadOnly: true}) defer ro1.Close() require.Nil(t, err) @@ -32,75 +30,16 @@ func TestNewGoLevelDB(t *testing.T) { require.Nil(t, err) } -func BenchmarkRandomReadsWrites(b *testing.B) { - b.StopTimer() - - numItems := int64(1000000) - internal := map[int64]int64{} - for i := 0; i < int(numItems); i++ { - internal[int64(i)] = int64(0) - } - db, err := NewGoLevelDB(fmt.Sprintf("test_%x", cmn.RandStr(12)), "") +func BenchmarkGoLevelDBRandomReadsWrites(b *testing.B) { + name := fmt.Sprintf("test_%x", cmn.RandStr(12)) + db, err := NewGoLevelDB(name, "") if err != nil { - b.Fatal(err.Error()) - return + b.Fatal(err) } + defer func() { + db.Close() + cleanupDBDir("", name) + }() - fmt.Println("ok, starting") - b.StartTimer() - - for i := 0; i < b.N; i++ { - // Write something - { - idx := (int64(cmn.RandInt()) % numItems) - internal[idx]++ - val := internal[idx] - idxBytes := int642Bytes(int64(idx)) - valBytes := int642Bytes(int64(val)) - //fmt.Printf("Set %X -> %X\n", idxBytes, valBytes) - db.Set( - idxBytes, - valBytes, - ) - } - // Read something - { - idx := (int64(cmn.RandInt()) % numItems) - val := internal[idx] - idxBytes := int642Bytes(int64(idx)) - valBytes := db.Get(idxBytes) - //fmt.Printf("Get %X -> %X\n", idxBytes, valBytes) - if val == 0 { - if !bytes.Equal(valBytes, nil) { - b.Errorf("Expected %v for %v, got %X", - nil, idx, valBytes) - break - } - } else { - if len(valBytes) != 8 { - b.Errorf("Expected length 8 for %v, got %X", - idx, valBytes) - break - } - valGot := bytes2Int64(valBytes) - if val != valGot { - b.Errorf("Expected %v for %v, got %v", - val, idx, valGot) - break - } - } - } - } - - db.Close() -} - -func int642Bytes(i int64) []byte { - buf := make([]byte, 8) - binary.BigEndian.PutUint64(buf, uint64(i)) - return buf -} - -func bytes2Int64(buf []byte) int64 { - return int64(binary.BigEndian.Uint64(buf)) + benchmarkRandomReadsWrites(b, db) } diff --git a/libs/db/mem_db.go b/libs/db/mem_db.go index ff516bc7d..fc567577d 100644 --- a/libs/db/mem_db.go +++ b/libs/db/mem_db.go @@ -7,7 +7,7 @@ import ( ) func init() { - registerDBCreator(MemDBBackend, func(name string, dir string) (DB, error) { + registerDBCreator(MemDBBackend, func(name, dir string) (DB, error) { return NewMemDB(), nil }, false) } diff --git a/libs/db/util_test.go b/libs/db/util_test.go index 07f9dd23e..39a02160c 100644 --- a/libs/db/util_test.go +++ b/libs/db/util_test.go @@ -22,6 +22,11 @@ func TestPrefixIteratorNoMatchNil(t *testing.T) { // Empty iterator for db populated after iterator created. func TestPrefixIteratorNoMatch1(t *testing.T) { for backend := range backends { + if backend == BoltDBBackend { + t.Log("bolt does not support concurrent writes while iterating") + continue + } + t.Run(fmt.Sprintf("Prefix w/ backend %s", backend), func(t *testing.T) { db, dir := newTempDB(t, backend) defer os.RemoveAll(dir) From 60b833403c2f4af0d9a4ab6f535f073f81f22653 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 6 May 2019 21:37:41 +0400 Subject: [PATCH 027/211] libs/db: close boltDBIterator (#3627) Refs https://github.com/tendermint/tendermint/pull/3610#discussion_r281201274 If we do not close, other txs will be stuck forever. --- libs/db/boltdb.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go index 6a31f11a9..d5b6f5864 100644 --- a/libs/db/boltdb.go +++ b/libs/db/boltdb.go @@ -190,31 +190,31 @@ func (bdb *boltDBBatch) WriteSync() { func (bdb *boltDBBatch) Close() {} -// WARNING: Any concurrent writes (Set, SetSync) will block until the Iterator -// is closed. +// WARNING: Any concurrent writes or reads will block until the iterator is +// closed. func (bdb *BoltDB) Iterator(start, end []byte) Iterator { tx, err := bdb.db.Begin(false) if err != nil { panic(err) } - c := tx.Bucket(bucket).Cursor() - return newBoltDBIterator(c, start, end, false) + return newBoltDBIterator(tx, start, end, false) } -// WARNING: Any concurrent writes (Set, SetSync) will block until the Iterator -// is closed. +// WARNING: Any concurrent writes or reads will block until the iterator is +// closed. func (bdb *BoltDB) ReverseIterator(start, end []byte) Iterator { tx, err := bdb.db.Begin(false) if err != nil { panic(err) } - c := tx.Bucket(bucket).Cursor() - return newBoltDBIterator(c, start, end, true) + return newBoltDBIterator(tx, start, end, true) } // boltDBIterator allows you to iterate on range of keys/values given some // start / end keys (nil & nil will result in doing full scan). type boltDBIterator struct { + tx *bbolt.Tx + itr *bbolt.Cursor start []byte end []byte @@ -226,7 +226,9 @@ type boltDBIterator struct { isReverse bool } -func newBoltDBIterator(itr *bbolt.Cursor, start, end []byte, isReverse bool) *boltDBIterator { +func newBoltDBIterator(tx *bbolt.Tx, start, end []byte, isReverse bool) *boltDBIterator { + itr := tx.Bucket(bucket).Cursor() + var ck, cv []byte if isReverse { if end == nil { @@ -244,6 +246,7 @@ func newBoltDBIterator(itr *bbolt.Cursor, start, end []byte, isReverse bool) *bo } return &boltDBIterator{ + tx: tx, itr: itr, start: start, end: end, @@ -304,8 +307,12 @@ func (itr *boltDBIterator) Value() []byte { return itr.currentValue } -// boltdb cursor has no close op. -func (itr *boltDBIterator) Close() {} +func (itr *boltDBIterator) Close() { + err := itr.tx.Rollback() + if err != nil { + panic(err) + } +} func (itr *boltDBIterator) assertIsValid() { if !itr.Valid() { From d0d9ef16f76a5845557cf3189dd6eef803a38aed Mon Sep 17 00:00:00 2001 From: Leo Date: Mon, 6 May 2019 20:10:39 +0000 Subject: [PATCH 028/211] libs/db: fix boltdb batching `[]byte` is unhashable and needs to be casted to a string. See https://github.com/tendermint/tendermint/pull/3610#discussion_r280995538 Ref https://github.com/tendermint/tendermint/issues/3626 --- libs/db/boltdb.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go index d5b6f5864..7ee988d2e 100644 --- a/libs/db/boltdb.go +++ b/libs/db/boltdb.go @@ -161,11 +161,11 @@ func (bdb *BoltDB) NewBatch() Batch { } func (bdb *boltDBBatch) Set(key, value []byte) { - bdb.buffer.Store(key, value) + bdb.buffer.Store(string(key), value) } func (bdb *boltDBBatch) Delete(key []byte) { - bdb.buffer.Delete(key) + bdb.buffer.Delete(string(key)) } // NOTE: the operation is synchronous (see BoltDB for reasons) @@ -174,7 +174,7 @@ func (bdb *boltDBBatch) Write() { b := tx.Bucket(bucket) var putErr error bdb.buffer.Range(func(key, value interface{}) bool { - putErr = b.Put(key.([]byte), value.([]byte)) + putErr = b.Put([]byte(key.(string)), value.([]byte)) return putErr == nil // stop if putErr is not nil }) return putErr From 1e073817de69eca661d99b024810e6d31ff0b50e Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 May 2019 11:09:06 +0400 Subject: [PATCH 029/211] rpc: /dial_peers: only mark peers as persistent if flag is on (#3620) ## Description also handle errors from DialPeersAsync remove nil addr from log msg fix TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode This is a follow-up from #3593 (review) Fixes most of the #3617, except #3593 (comment) ## Commits * rpc: /dial_peers: only mark peers as persistent if flag is on also - handle errors from DialPeersAsync - remove nil addr from log msg - fix TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode This is a follow-up from https://github.com/tendermint/tendermint/pull/3593#pullrequestreview-233556909 * remove a call to AddPersistentPeers TestDialFail will trigger a reconnect --- node/node.go | 6 ++++-- p2p/pex/pex_reactor_test.go | 6 +++++- p2p/switch.go | 2 +- p2p/switch_test.go | 3 +-- rpc/core/net.go | 9 ++++++--- 5 files changed, 17 insertions(+), 9 deletions(-) diff --git a/node/node.go b/node/node.go index 1221e8b55..2d7b3b3bb 100644 --- a/node/node.go +++ b/node/node.go @@ -682,8 +682,10 @@ func (n *Node) OnStart() error { } // Always connect to persistent peers - // parsing errors are handled above by AddPersistentPeers - _ = n.sw.DialPeersAsync(splitAndTrimEmpty(n.config.P2P.PersistentPeers, ",", " ")) + err = n.sw.DialPeersAsync(splitAndTrimEmpty(n.config.P2P.PersistentPeers, ",", " ")) + if err != nil { + return errors.Wrap(err, "could not dial peers from persistent_peers field") + } return nil } diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index 8c52a25ee..b16a0d919 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -330,7 +330,8 @@ func TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode(t *testing.T) { require.Nil(t, err) defer os.RemoveAll(dir) // nolint: errcheck - pexR, book := createReactor(&PEXReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 1 * time.Millisecond}) + pexRConfig := &PEXReactorConfig{SeedMode: true, SeedDisconnectWaitPeriod: 1 * time.Millisecond} + pexR, book := createReactor(pexRConfig) defer teardownReactor(book) sw := createSwitchAndAddReactors(pexR) @@ -353,6 +354,9 @@ func TestPEXReactorDoesNotDisconnectFromPersistentPeerInSeedMode(t *testing.T) { assert.Equal(t, 1, sw.Peers().Size()) assert.True(t, sw.Peers().Has(peerSwitch.NodeInfo().ID())) + // sleep for SeedDisconnectWaitPeriod + time.Sleep(pexRConfig.SeedDisconnectWaitPeriod + 1*time.Millisecond) + // 2. attemptDisconnects should not disconnect because the peer is persistent pexR.attemptDisconnects() assert.Equal(t, 1, sw.Peers().Size()) diff --git a/p2p/switch.go b/p2p/switch.go index ccb7119d2..66e90bec5 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -308,7 +308,7 @@ func (sw *Switch) StopPeerForError(peer Peer, reason interface{}) { addr, err = peer.NodeInfo().NetAddress() if err != nil { sw.Logger.Error("Wanted to reconnect to inbound peer, but self-reported address is wrong", - "peer", peer, "addr", addr, "err", err) + "peer", peer, "err", err) return } } diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 6c7538b51..8485a759f 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -410,7 +410,6 @@ func TestSwitchReconnectsToOutboundPersistentPeer(t *testing.T) { err = sw.DialPeerWithAddress(rp.Addr()) require.Nil(t, err) - time.Sleep(50 * time.Millisecond) require.NotNil(t, sw.Peers().Get(rp.ID())) p := sw.Peers().List()[0] @@ -432,7 +431,7 @@ func TestSwitchReconnectsToOutboundPersistentPeer(t *testing.T) { defer rp.Stop() conf := config.DefaultP2PConfig() - conf.TestDialFail = true + conf.TestDialFail = true // will trigger a reconnect err = sw.addOutboundPeerWithConfig(rp.Addr(), conf) require.NotNil(t, err) // DialPeerWithAddres - sw.peerConfig resets the dialer diff --git a/rpc/core/net.go b/rpc/core/net.go index 48bf576a8..165230619 100644 --- a/rpc/core/net.go +++ b/rpc/core/net.go @@ -196,11 +196,14 @@ func UnsafeDialPeers(ctx *rpctypes.Context, peers []string, persistent bool) (*c return &ctypes.ResultDialPeers{}, errors.New("No peers provided") } logger.Info("DialPeers", "peers", peers, "persistent", persistent) - if err := p2pPeers.AddPersistentPeers(peers); err != nil { + if persistent { + if err := p2pPeers.AddPersistentPeers(peers); err != nil { + return &ctypes.ResultDialPeers{}, err + } + } + if err := p2pPeers.DialPeersAsync(peers); err != nil { return &ctypes.ResultDialPeers{}, err } - // parsing errors are handled above by AddPersistentPeers - _ = p2pPeers.DialPeersAsync(peers) return &ctypes.ResultDialPeers{Log: "Dialing peers in progress. See /net_info for details"}, nil } From 27909e5d2aee605a7cb562a53195b2e99d78eb8e Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 May 2019 12:25:35 +0400 Subject: [PATCH 030/211] mempool: remove only valid (Code==0) txs on Update (#3625) * mempool: remove only valid (Code==0) txs on Update so evil proposers can't drop valid txs in Commit stage. Also remove invalid (Code!=0) txs from the cache so they can be resubmitted. Fixes #3322 @rickyyangz: In the end of commit stage, we will update mempool to remove all the txs in current block. // Update mempool. err = blockExec.mempool.Update( block.Height, block.Txs, TxPreCheck(state), TxPostCheck(state), ) Assum an account has 3 transactions in the mempool, the sequences are 100, 101 and 102 separately, So an evil proposal can only package the 101 and 102 transactions into its proposal block, and leave 100 still in mempool, then the two txs will be removed from all validators' mempool when commit. So the account lost the two valid txs. @ebuchman: In the longer term we may want to do something like #2639 so we can validate txs before we commit the block. But even in this case we'd only want to run the equivalent of CheckTx, which means the DeliverTx could still fail even if the CheckTx passes depending on how the app handles the ABCI Code semantics. So more work will be required around the ABCI code. See also #2185 * add changelog entry and tests * improve changelog message * reformat code --- CHANGELOG_PENDING.md | 4 ++ mempool/cache_test.go | 3 +- mempool/clist_mempool.go | 70 +++++++++++++++++------------------ mempool/clist_mempool_test.go | 50 +++++++++++++++++++------ mempool/mempool.go | 2 +- mock/mempool.go | 1 + state/execution.go | 4 +- 7 files changed, 84 insertions(+), 50 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 3b6dffb3a..d03d98112 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -48,3 +48,7 @@ * `Switch#DialPeerWithAddress` now only takes an address - [consensus] \#3067 getBeginBlockValidatorInfo loads validators from stateDB instead of state (@james-ray) - [pex] \#3603 Dial seeds when addrbook needs more addresses (@defunctzombie) +- [mempool] \#3322 Remove only valid (Code==0) txs on Update + * `Mempool#Update` and `BlockExecutor#Commit` now accept + `[]*abci.ResponseDeliverTx` - list of `DeliverTx` responses, which should + match `block.Txs` diff --git a/mempool/cache_test.go b/mempool/cache_test.go index ea9f63fd6..539bf1197 100644 --- a/mempool/cache_test.go +++ b/mempool/cache_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/abci/example/kvstore" + abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/proxy" "github.com/tendermint/tendermint/types" ) @@ -66,7 +67,7 @@ func TestCacheAfterUpdate(t *testing.T) { tx := types.Tx{byte(v)} updateTxs = append(updateTxs, tx) } - mempool.Update(int64(tcIndex), updateTxs, nil, nil) + mempool.Update(int64(tcIndex), updateTxs, abciResponses(len(updateTxs), abci.CodeTypeOK), nil, nil) for _, v := range tc.reAddIndices { tx := types.Tx{byte(v)} diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 2a4bacbbb..c6ff475aa 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -519,6 +519,7 @@ func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { func (mem *CListMempool) Update( height int64, txs types.Txs, + deliverTxResponses []*abci.ResponseDeliverTx, preCheck PreCheckFunc, postCheck PostCheckFunc, ) error { @@ -533,20 +534,37 @@ func (mem *CListMempool) Update( mem.postCheck = postCheck } - // Add committed transactions to cache (if missing). - for _, tx := range txs { - _ = mem.cache.Push(tx) - } + for i, tx := range txs { + if deliverTxResponses[i].Code == abci.CodeTypeOK { + // Add valid committed tx to the cache (if missing). + _ = mem.cache.Push(tx) + + // Remove valid committed tx from the mempool. + if e, ok := mem.txsMap.Load(txKey(tx)); ok { + mem.removeTx(tx, e.(*clist.CElement), false) + } + } else { + // Allow invalid transactions to be resubmitted. + mem.cache.Remove(tx) - // Remove committed transactions. - txsLeft := mem.removeTxs(txs) + // Don't remove invalid tx from the mempool. + // Otherwise evil proposer can drop valid txs. + // Example: + // 100 -> 101 -> 102 + // Block, proposed by evil proposer: + // 101 -> 102 + // Mempool (if you remove txs): + // 100 + // https://github.com/tendermint/tendermint/issues/3322. + } + } // Either recheck non-committed txs to see if they became invalid // or just notify there're some txs left. - if len(txsLeft) > 0 { + if mem.Size() > 0 { if mem.config.Recheck { - mem.logger.Info("Recheck txs", "numtxs", len(txsLeft), "height", height) - mem.recheckTxs(txsLeft) + mem.logger.Info("Recheck txs", "numtxs", mem.Size(), "height", height) + mem.recheckTxs() // At this point, mem.txs are being rechecked. // mem.recheckCursor re-scans mem.txs and possibly removes some txs. // Before mem.Reap(), we should wait for mem.recheckCursor to be nil. @@ -561,42 +579,22 @@ func (mem *CListMempool) Update( return nil } -func (mem *CListMempool) removeTxs(txs types.Txs) []types.Tx { - // Build a map for faster lookups. - txsMap := make(map[string]struct{}, len(txs)) - for _, tx := range txs { - txsMap[string(tx)] = struct{}{} - } - - txsLeft := make([]types.Tx, 0, mem.txs.Len()) - for e := mem.txs.Front(); e != nil; e = e.Next() { - memTx := e.Value.(*mempoolTx) - // Remove the tx if it's already in a block. - if _, ok := txsMap[string(memTx.tx)]; ok { - // NOTE: we don't remove committed txs from the cache. - mem.removeTx(memTx.tx, e, false) - - continue - } - txsLeft = append(txsLeft, memTx.tx) +func (mem *CListMempool) recheckTxs() { + if mem.Size() == 0 { + panic("recheckTxs is called, but the mempool is empty") } - return txsLeft -} -// NOTE: pass in txs because mem.txs can mutate concurrently. -func (mem *CListMempool) recheckTxs(txs []types.Tx) { - if len(txs) == 0 { - return - } atomic.StoreInt32(&mem.rechecking, 1) mem.recheckCursor = mem.txs.Front() mem.recheckEnd = mem.txs.Back() // Push txs to proxyAppConn // NOTE: globalCb may be called concurrently. - for _, tx := range txs { - mem.proxyAppConn.CheckTxAsync(tx) + for e := mem.txs.Front(); e != nil; e = e.Next() { + memTx := e.Value.(*mempoolTx) + mem.proxyAppConn.CheckTxAsync(memTx.tx) } + mem.proxyAppConn.FlushAsync() } diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index 69d4eb68c..3eb4990b6 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -170,22 +170,42 @@ func TestMempoolFilters(t *testing.T) { {10, PreCheckAminoMaxBytes(22), PostCheckMaxGas(0), 0}, } for tcIndex, tt := range tests { - mempool.Update(1, emptyTxArr, tt.preFilter, tt.postFilter) + mempool.Update(1, emptyTxArr, abciResponses(len(emptyTxArr), abci.CodeTypeOK), tt.preFilter, tt.postFilter) checkTxs(t, mempool, tt.numTxsToCreate, UnknownPeerID) require.Equal(t, tt.expectedNumTxs, mempool.Size(), "mempool had the incorrect size, on test case %d", tcIndex) mempool.Flush() } } -func TestMempoolUpdateAddsTxsToCache(t *testing.T) { +func TestMempoolUpdate(t *testing.T) { app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) mempool, cleanup := newMempoolWithApp(cc) defer cleanup() - mempool.Update(1, []types.Tx{[]byte{0x01}}, nil, nil) - err := mempool.CheckTx([]byte{0x01}, nil) - if assert.Error(t, err) { - assert.Equal(t, ErrTxInCache, err) + + // 1. Adds valid txs to the cache + { + mempool.Update(1, []types.Tx{[]byte{0x01}}, abciResponses(1, abci.CodeTypeOK), nil, nil) + err := mempool.CheckTx([]byte{0x01}, nil) + if assert.Error(t, err) { + assert.Equal(t, ErrTxInCache, err) + } + } + + // 2. Removes valid txs from the mempool + { + err := mempool.CheckTx([]byte{0x02}, nil) + require.NoError(t, err) + mempool.Update(1, []types.Tx{[]byte{0x02}}, abciResponses(1, abci.CodeTypeOK), nil, nil) + assert.Zero(t, mempool.Size()) + } + + // 3. Removes invalid transactions from the cache, but leaves them in the mempool (if present) + { + err := mempool.CheckTx([]byte{0x03}, nil) + require.NoError(t, err) + mempool.Update(1, []types.Tx{[]byte{0x03}}, abciResponses(1, 1), nil, nil) + assert.Equal(t, 1, mempool.Size()) } } @@ -210,7 +230,7 @@ func TestTxsAvailable(t *testing.T) { // it should fire once now for the new height // since there are still txs left committedTxs, txs := txs[:50], txs[50:] - if err := mempool.Update(1, committedTxs, nil, nil); err != nil { + if err := mempool.Update(1, committedTxs, abciResponses(len(committedTxs), abci.CodeTypeOK), nil, nil); err != nil { t.Error(err) } ensureFire(t, mempool.TxsAvailable(), timeoutMS) @@ -222,7 +242,7 @@ func TestTxsAvailable(t *testing.T) { // now call update with all the txs. it should not fire as there are no txs left committedTxs = append(txs, moreTxs...) - if err := mempool.Update(2, committedTxs, nil, nil); err != nil { + if err := mempool.Update(2, committedTxs, abciResponses(len(committedTxs), abci.CodeTypeOK), nil, nil); err != nil { t.Error(err) } ensureNoFire(t, mempool.TxsAvailable(), timeoutMS) @@ -281,7 +301,7 @@ func TestSerialReap(t *testing.T) { binary.BigEndian.PutUint64(txBytes, uint64(i)) txs = append(txs, txBytes) } - if err := mempool.Update(0, txs, nil, nil); err != nil { + if err := mempool.Update(0, txs, abciResponses(len(txs), abci.CodeTypeOK), nil, nil); err != nil { t.Error(err) } } @@ -462,7 +482,7 @@ func TestMempoolTxsBytes(t *testing.T) { assert.EqualValues(t, 1, mempool.TxsBytes()) // 3. zero again after tx is removed by Update - mempool.Update(1, []types.Tx{[]byte{0x01}}, nil, nil) + mempool.Update(1, []types.Tx{[]byte{0x01}}, abciResponses(1, abci.CodeTypeOK), nil, nil) assert.EqualValues(t, 0, mempool.TxsBytes()) // 4. zero after Flush @@ -507,7 +527,7 @@ func TestMempoolTxsBytes(t *testing.T) { require.NotEmpty(t, res2.Data) // Pretend like we committed nothing so txBytes gets rechecked and removed. - mempool.Update(1, []types.Tx{}, nil, nil) + mempool.Update(1, []types.Tx{}, abciResponses(0, abci.CodeTypeOK), nil, nil) assert.EqualValues(t, 0, mempool.TxsBytes()) } @@ -570,3 +590,11 @@ func checksumFile(p string, t *testing.T) string { require.Nil(t, err, "expecting successful read of %q", p) return checksumIt(data) } + +func abciResponses(n int, code uint32) []*abci.ResponseDeliverTx { + responses := make([]*abci.ResponseDeliverTx, 0, n) + for i := 0; i < n; i++ { + responses = append(responses, &abci.ResponseDeliverTx{Code: code}) + } + return responses +} diff --git a/mempool/mempool.go b/mempool/mempool.go index 1c1873d47..0995c734f 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -43,7 +43,7 @@ type Mempool interface { // Update informs the mempool that the given txs were committed and can be discarded. // NOTE: this should be called *after* block is committed by consensus. // NOTE: unsafe; Lock/Unlock must be managed by caller - Update(blockHeight int64, blockTxs types.Txs, newPreFn PreCheckFunc, newPostFn PostCheckFunc) error + Update(blockHeight int64, blockTxs types.Txs, deliverTxResponses []*abci.ResponseDeliverTx, newPreFn PreCheckFunc, newPostFn PostCheckFunc) error // FlushAppConn flushes the mempool connection to ensure async reqResCb calls are // done. E.g. from CheckTx. diff --git a/mock/mempool.go b/mock/mempool.go index 66ad68d30..cebe156ba 100644 --- a/mock/mempool.go +++ b/mock/mempool.go @@ -27,6 +27,7 @@ func (Mempool) ReapMaxTxs(n int) types.Txs { return types.Txs{} } func (Mempool) Update( _ int64, _ types.Txs, + _ []*abci.ResponseDeliverTx, _ mempl.PreCheckFunc, _ mempl.PostCheckFunc, ) error { diff --git a/state/execution.go b/state/execution.go index d872145ef..7e49a9ad8 100644 --- a/state/execution.go +++ b/state/execution.go @@ -156,7 +156,7 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b } // Lock mempool, commit app state, update mempoool. - appHash, err := blockExec.Commit(state, block) + appHash, err := blockExec.Commit(state, block, abciResponses.DeliverTx) if err != nil { return state, fmt.Errorf("Commit failed for application: %v", err) } @@ -188,6 +188,7 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b func (blockExec *BlockExecutor) Commit( state State, block *types.Block, + deliverTxResponses []*abci.ResponseDeliverTx, ) ([]byte, error) { blockExec.mempool.Lock() defer blockExec.mempool.Unlock() @@ -222,6 +223,7 @@ func (blockExec *BlockExecutor) Commit( err = blockExec.mempool.Update( block.Height, block.Txs, + deliverTxResponses, TxPreCheck(state), TxPostCheck(state), ) From a7358bc69f63a9e8afdb85df1f3f3080965c1f38 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 May 2019 12:33:47 +0400 Subject: [PATCH 031/211] libs/db: conditional compilation (#3628) * libs/db: conditional compilation For cleveldb: go build -tags cleveldb For boltdb: go build -tags boltdb Fixes #3611 * document db_backend param better * remove deprecated LevelDBBackend * update changelog * add missing lines * add new line * fix TestRemoteDB * add a line about boltdb tag * Revert "remove deprecated LevelDBBackend" This reverts commit 1aa85453f76605e0c4d967601bbe26240e668d51. * make PR non breaking * change DEPRECATED label format https://stackoverflow.com/a/36360323/820520 --- CHANGELOG_PENDING.md | 5 ++++ Makefile | 6 ++--- config/config.go | 15 ++++++++++-- config/toml.go | 13 +++++++++- docs/introduction/install.md | 12 ++++----- docs/tendermint-core/configuration.md | 13 +++++++++- libs/db/backend_test.go | 1 + libs/db/boltdb.go | 2 ++ libs/db/boltdb_test.go | 2 ++ libs/db/c_level_db.go | 2 +- libs/db/c_level_db_test.go | 6 ++--- libs/db/db.go | 35 +++++++++++++++++++++------ libs/db/remotedb/remotedb_test.go | 2 +- lite/proxy/verifier.go | 2 +- 14 files changed, 88 insertions(+), 28 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d03d98112..0067c8c15 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -38,6 +38,11 @@ - [rpc] \#3362 `/dial_seeds` & `/dial_peers` return errors if addresses are incorrect (except when IP lookup fails) - [node] \#3362 returns an error if `persistent_peers` list is invalid (except when IP lookup fails) - [p2p] \#3531 Terminate session on nonce wrapping (@climber73) +- [libs/db] \#3611 Conditional compilation + * Use `cleveldb` tag instead of `gcc` to compile Tendermint with CLevelDB or + use `make build_c` / `make install_c` (full instructions can be found at + https://tendermint.com/docs/introduction/install.html#compile-with-cleveldb-support) + * Use `boltdb` tag to compile Tendermint with bolt db ### BUG FIXES: - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode diff --git a/Makefile b/Makefile index 7c2ce1d9c..c5bde692d 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,7 @@ build: CGO_ENABLED=0 go build $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o $(OUTPUT) ./cmd/tendermint/ build_c: - CGO_ENABLED=1 go build $(BUILD_FLAGS) -tags "$(BUILD_TAGS) gcc" -o $(OUTPUT) ./cmd/tendermint/ + CGO_ENABLED=1 go build $(BUILD_FLAGS) -tags "$(BUILD_TAGS) cleveldb" -o $(OUTPUT) ./cmd/tendermint/ build_race: CGO_ENABLED=0 go build -race $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o $(OUTPUT) ./cmd/tendermint @@ -32,7 +32,7 @@ install: CGO_ENABLED=0 go install $(BUILD_FLAGS) -tags $(BUILD_TAGS) ./cmd/tendermint install_c: - CGO_ENABLED=1 go install $(BUILD_FLAGS) -tags "$(BUILD_TAGS) gcc" ./cmd/tendermint + CGO_ENABLED=1 go install $(BUILD_FLAGS) -tags "$(BUILD_TAGS) cleveldb" ./cmd/tendermint ######################################## ### Protobuf @@ -132,7 +132,7 @@ clean_certs: rm -f db/remotedb/::.crt db/remotedb/::.key test_libs: gen_certs - go test -tags gcc $(PACKAGES) + go test -tags clevedb boltdb $(PACKAGES) make clean_certs grpc_dbserver: diff --git a/config/config.go b/config/config.go index 3ac22adbf..6c4654fed 100644 --- a/config/config.go +++ b/config/config.go @@ -153,7 +153,18 @@ type BaseConfig struct { // and verifying their commits FastSync bool `mapstructure:"fast_sync"` - // Database backend: leveldb | memdb | cleveldb + // Database backend: goleveldb | cleveldb | boltdb + // * goleveldb (github.com/syndtr/goleveldb - most popular implementation) + // - pure go + // - stable + // * cleveldb (uses levigo wrapper) + // - fast + // - requires gcc + // - use cleveldb build tag (go build -tags cleveldb) + // * boltdb (uses etcd's fork of bolt - github.com/etcd-io/bbolt) + // - EXPERIMENTAL + // - may be faster is some use-cases (random reads - indexer) + // - use boltdb build tag (go build -tags boltdb) DBBackend string `mapstructure:"db_backend"` // Database directory @@ -207,7 +218,7 @@ func DefaultBaseConfig() BaseConfig { ProfListenAddress: "", FastSync: true, FilterPeers: false, - DBBackend: "leveldb", + DBBackend: "goleveldb", DBPath: "data", } } diff --git a/config/toml.go b/config/toml.go index f80f525e4..b1541c05a 100644 --- a/config/toml.go +++ b/config/toml.go @@ -81,7 +81,18 @@ moniker = "{{ .BaseConfig.Moniker }}" # and verifying their commits fast_sync = {{ .BaseConfig.FastSync }} -# Database backend: leveldb | memdb | cleveldb +# Database backend: goleveldb | cleveldb | boltdb +# * goleveldb (github.com/syndtr/goleveldb - most popular implementation) +# - pure go +# - stable +# * cleveldb (uses levigo wrapper) +# - fast +# - requires gcc +# - use cleveldb build tag (go build -tags cleveldb) +# * boltdb (uses etcd's fork of bolt - github.com/etcd-io/bbolt) +# - EXPERIMENTAL +# - may be faster is some use-cases (random reads - indexer) +# - use boltdb build tag (go build -tags boltdb) db_backend = "{{ .BaseConfig.DBBackend }}" # Database directory diff --git a/docs/introduction/install.md b/docs/introduction/install.md index 3005a7349..e96135826 100644 --- a/docs/introduction/install.md +++ b/docs/introduction/install.md @@ -79,9 +79,7 @@ make install Install [LevelDB](https://github.com/google/leveldb) (minimum version is 1.7). -### Ubuntu - -Install LevelDB with snappy (optionally): +Install LevelDB with snappy (optionally). Below are commands for Ubuntu: ``` sudo apt-get update @@ -100,23 +98,23 @@ wget https://github.com/google/leveldb/archive/v1.20.tar.gz && \ rm -f v1.20.tar.gz ``` -Set database backend to cleveldb: +Set a database backend to `cleveldb`: ``` # config/config.toml db_backend = "cleveldb" ``` -To install Tendermint, run +To install Tendermint, run: ``` CGO_LDFLAGS="-lsnappy" make install_c ``` -or run +or run: ``` CGO_LDFLAGS="-lsnappy" make build_c ``` -to put the binary in `./build`. +which puts the binary in `./build`. diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index d19c272fc..cdbdf1fe2 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -30,7 +30,18 @@ moniker = "anonymous" # and verifying their commits fast_sync = true -# Database backend: leveldb | memdb | cleveldb +# Database backend: goleveldb | cleveldb | boltdb +# * goleveldb (github.com/syndtr/goleveldb - most popular implementation) +# - pure go +# - stable +# * cleveldb (uses levigo wrapper) +# - fast +# - requires gcc +# - use cleveldb build tag (go build -tags cleveldb) +# * boltdb (uses etcd's fork of bolt - github.com/etcd-io/bbolt) +# - EXPERIMENTAL +# - may be faster is some use-cases (random reads - indexer) +# - use boltdb build tag (go build -tags boltdb) db_backend = "leveldb" # Database directory diff --git a/libs/db/backend_test.go b/libs/db/backend_test.go index fb2a3d0b3..d755a6f27 100644 --- a/libs/db/backend_test.go +++ b/libs/db/backend_test.go @@ -9,6 +9,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + cmn "github.com/tendermint/tendermint/libs/common" ) diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go index 7ee988d2e..f53d2785c 100644 --- a/libs/db/boltdb.go +++ b/libs/db/boltdb.go @@ -1,3 +1,5 @@ +// +build boltdb + package db import ( diff --git a/libs/db/boltdb_test.go b/libs/db/boltdb_test.go index ced57d2e0..416a8fd03 100644 --- a/libs/db/boltdb_test.go +++ b/libs/db/boltdb_test.go @@ -1,3 +1,5 @@ +// +build boltdb + package db import ( diff --git a/libs/db/c_level_db.go b/libs/db/c_level_db.go index 116e51bc2..0ac040d20 100644 --- a/libs/db/c_level_db.go +++ b/libs/db/c_level_db.go @@ -1,4 +1,4 @@ -// +build gcc +// +build cleveldb package db diff --git a/libs/db/c_level_db_test.go b/libs/db/c_level_db_test.go index e71dee0c1..1c10fcdef 100644 --- a/libs/db/c_level_db_test.go +++ b/libs/db/c_level_db_test.go @@ -1,4 +1,4 @@ -// +build gcc +// +build cleveldb package db @@ -93,7 +93,7 @@ func TestCLevelDBBackend(t *testing.T) { // Can't use "" (current directory) or "./" here because levigo.Open returns: // "Error initializing DB: IO error: test_XXX.db: Invalid argument" dir := os.TempDir() - db := NewDB(name, LevelDBBackend, dir) + db := NewDB(name, CLevelDBBackend, dir) defer cleanupDBDir(dir, name) _, ok := db.(*CLevelDB) @@ -103,7 +103,7 @@ func TestCLevelDBBackend(t *testing.T) { func TestCLevelDBStats(t *testing.T) { name := fmt.Sprintf("test_%x", cmn.RandStr(12)) dir := os.TempDir() - db := NewDB(name, LevelDBBackend, dir) + db := NewDB(name, CLevelDBBackend, dir) defer cleanupDBDir(dir, name) assert.NotEmpty(t, db.Stats()) diff --git a/libs/db/db.go b/libs/db/db.go index 43b788281..b68204420 100644 --- a/libs/db/db.go +++ b/libs/db/db.go @@ -5,18 +5,37 @@ import ( "strings" ) -//---------------------------------------- -// Main entry - type DBBackendType string +// These are valid backend types. const ( - LevelDBBackend DBBackendType = "leveldb" // legacy, defaults to goleveldb unless +gcc - CLevelDBBackend DBBackendType = "cleveldb" + // LevelDBBackend is a legacy type. Defaults to goleveldb unless cleveldb + // build tag was used, in which it becomes cleveldb. + // Deprecated: Use concrete types (golevedb, cleveldb, etc.) + LevelDBBackend DBBackendType = "leveldb" + // GoLevelDBBackend represents goleveldb (github.com/syndtr/goleveldb - most + // popular implementation) + // - pure go + // - stable GoLevelDBBackend DBBackendType = "goleveldb" - MemDBBackend DBBackendType = "memdb" - FSDBBackend DBBackendType = "fsdb" // using the filesystem naively - BoltDBBackend DBBackendType = "boltdb" + // CLevelDBBackend represents cleveldb (uses levigo wrapper) + // - fast + // - requires gcc + // - use cleveldb build tag (go build -tags cleveldb) + CLevelDBBackend DBBackendType = "cleveldb" + // MemDBBackend represents in-memoty key value store, which is mostly used + // for testing. + MemDBBackend DBBackendType = "memdb" + // FSDBBackend represents filesystem database + // - EXPERIMENTAL + // - slow + FSDBBackend DBBackendType = "fsdb" + // BoltDBBackend represents bolt (uses etcd's fork of bolt - + // github.com/etcd-io/bbolt) + // - EXPERIMENTAL + // - may be faster is some use-cases (random reads - indexer) + // - use boltdb build tag (go build -tags boltdb) + BoltDBBackend DBBackendType = "boltdb" ) type dbCreator func(name string, dir string) (DB, error) diff --git a/libs/db/remotedb/remotedb_test.go b/libs/db/remotedb/remotedb_test.go index f5c8e2cb5..43a022461 100644 --- a/libs/db/remotedb/remotedb_test.go +++ b/libs/db/remotedb/remotedb_test.go @@ -28,7 +28,7 @@ func TestRemoteDB(t *testing.T) { client, err := remotedb.NewRemoteDB(ln.Addr().String(), cert) require.Nil(t, err, "expecting a successful client creation") dbName := "test-remote-db" - require.Nil(t, client.InitRemote(&remotedb.Init{Name: dbName, Type: "leveldb"})) + require.Nil(t, client.InitRemote(&remotedb.Init{Name: dbName, Type: "goleveldb"})) defer func() { err := os.RemoveAll(dbName + ".db") if err != nil { diff --git a/lite/proxy/verifier.go b/lite/proxy/verifier.go index b7c11f18e..429c54b2d 100644 --- a/lite/proxy/verifier.go +++ b/lite/proxy/verifier.go @@ -14,7 +14,7 @@ func NewVerifier(chainID, rootDir string, client lclient.SignStatusClient, logge logger.Info("lite/proxy/NewVerifier()...", "chainID", chainID, "rootDir", rootDir, "client", client) memProvider := lite.NewDBProvider("trusted.mem", dbm.NewMemDB()).SetLimit(cacheSize) - lvlProvider := lite.NewDBProvider("trusted.lvl", dbm.NewDB("trust-base", dbm.LevelDBBackend, rootDir)) + lvlProvider := lite.NewDBProvider("trusted.lvl", dbm.NewDB("trust-base", dbm.GoLevelDBBackend, rootDir)) trust := lite.NewMultiProvider( memProvider, lvlProvider, From a076b4820219313c7b388df10c8664042d8711dc Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 7 May 2019 14:06:20 +0400 Subject: [PATCH 032/211] libs/db: boltdb: use slice instead of sync.Map (#3633) for storing batch keys and values in boltDBBatch. NOTE: batch does not have to be safe for concurrent access. Delete may be slow, but given it should not be used often, it's ok. Fixes #3631 --- libs/db/boltdb.go | 47 ++++++++++++++++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 13 deletions(-) diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go index f53d2785c..6bf474c86 100644 --- a/libs/db/boltdb.go +++ b/libs/db/boltdb.go @@ -8,7 +8,6 @@ import ( "fmt" "os" "path/filepath" - "sync" "github.com/etcd-io/bbolt" ) @@ -150,36 +149,58 @@ func (bdb *BoltDB) Stats() map[string]string { // boltDBBatch stores key values in sync.Map and dumps them to the underlying // DB upon Write call. type boltDBBatch struct { - buffer *sync.Map - db *BoltDB + buffer []struct { + k []byte + v []byte + } + db *BoltDB } // NewBatch returns a new batch. func (bdb *BoltDB) NewBatch() Batch { return &boltDBBatch{ - buffer: &sync.Map{}, - db: bdb, + buffer: make([]struct { + k []byte + v []byte + }, 0), + db: bdb, } } +// It is safe to modify the contents of the argument after Set returns but not +// before. func (bdb *boltDBBatch) Set(key, value []byte) { - bdb.buffer.Store(string(key), value) + bdb.buffer = append(bdb.buffer, struct { + k []byte + v []byte + }{ + key, value, + }) } +// It is safe to modify the contents of the argument after Delete returns but +// not before. func (bdb *boltDBBatch) Delete(key []byte) { - bdb.buffer.Delete(string(key)) + for i, elem := range bdb.buffer { + if bytes.Equal(elem.k, key) { + // delete without preserving order + bdb.buffer[i] = bdb.buffer[len(bdb.buffer)-1] + bdb.buffer = bdb.buffer[:len(bdb.buffer)-1] + return + } + } } // NOTE: the operation is synchronous (see BoltDB for reasons) func (bdb *boltDBBatch) Write() { err := bdb.db.db.Batch(func(tx *bbolt.Tx) error { b := tx.Bucket(bucket) - var putErr error - bdb.buffer.Range(func(key, value interface{}) bool { - putErr = b.Put([]byte(key.(string)), value.([]byte)) - return putErr == nil // stop if putErr is not nil - }) - return putErr + for _, elem := range bdb.buffer { + if putErr := b.Put(elem.k, elem.v); putErr != nil { + return putErr + } + } + return nil }) if err != nil { panic(err) From 6f1ccb6c4959be3a29ddcf9a3584bc9305a5e4c5 Mon Sep 17 00:00:00 2001 From: Zarko Milosevic Date: Fri, 10 May 2019 19:37:21 +0200 Subject: [PATCH 033/211] [ADR] ADR-36: blockchain reactor refactor spec (#3506) * Add blockchain reactor refactor ADR * docs: Correct markdown and go syntax in adr-036 * Update docs/architecture/adr-036-blockchain-reactor-refactor.md Co-Authored-By: milosevic * Add comments and address reviewer comments * Improve formating * Small improvements * adr-036 -> adr-040 --- .../adr-040-blockchain-reactor-refactor.md | 534 ++++++++++++++++++ docs/architecture/img/bc-reactor-refactor.png | Bin 0 -> 8105 bytes docs/architecture/img/bc-reactor.png | Bin 0 -> 44211 bytes docs/tendermint-core/configuration.md | 2 +- docs/tendermint-core/running-in-production.md | 2 +- 5 files changed, 536 insertions(+), 2 deletions(-) create mode 100644 docs/architecture/adr-040-blockchain-reactor-refactor.md create mode 100644 docs/architecture/img/bc-reactor-refactor.png create mode 100644 docs/architecture/img/bc-reactor.png diff --git a/docs/architecture/adr-040-blockchain-reactor-refactor.md b/docs/architecture/adr-040-blockchain-reactor-refactor.md new file mode 100644 index 000000000..520d55b5d --- /dev/null +++ b/docs/architecture/adr-040-blockchain-reactor-refactor.md @@ -0,0 +1,534 @@ +# ADR 040: Blockchain Reactor Refactor + +## Changelog + +19-03-2019: Initial draft + +## Context + +The Blockchain Reactor's high level responsibility is to enable peers who are far behind the current state of the +blockchain to quickly catch up by downloading many blocks in parallel from its peers, verifying block correctness, and +executing them against the ABCI application. We call the protocol executed by the Blockchain Reactor `fast-sync`. +The current architecture diagram of the blockchain reactor can be found here: + +![Blockchain Reactor Architecture Diagram](img/bc-reactor.png) + +The current architecture consists of dozens of routines and it is tightly depending on the `Switch`, making writing +unit tests almost impossible. Current tests require setting up complex dependency graphs and dealing with concurrency. +Note that having dozens of routines is in this case overkill as most of the time routines sits idle waiting for +something to happen (message to arrive or timeout to expire). Due to dependency on the `Switch`, testing relatively +complex network scenarios and failures (for example adding and removing peers) is very complex tasks and frequently lead +to complex tests with not deterministic behavior ([#3400]). Impossibility to write proper tests makes confidence in +the code low and this resulted in several issues (some are fixed in the meantime and some are still open): +[#3400], [#2897], [#2896], [#2699], [#2888], [#2457], [#2622], [#2026]. + +## Decision + +To remedy these issues we plan a major refactor of the blockchain reactor. The proposed architecture is largely inspired +by ADR-30 and is presented on the following diagram: +![Blockchain Reactor Refactor Diagram](img/bc-reactor-refactor.png) + +We suggest a concurrency architecture where the core algorithm (we call it `Controller`) is extracted into a finite +state machine. The active routine of the reactor is called `Executor` and is responsible for receiving and sending +messages from/to peers and triggering timeouts. What messages should be sent and timeouts triggered is determined mostly +by the `Controller`. The exception is `Peer Heartbeat` mechanism which is `Executor` responsibility. The heartbeat +mechanism is used to remove slow and unresponsive peers from the peer list. Writing of unit tests is simpler with +this architecture as most of the critical logic is part of the `Controller` function. We expect that simpler concurrency +architecture will not have significant negative effect on the performance of this reactor (to be confirmed by +experimental evaluation). + + +### Implementation changes + +We assume the following system model for "fast sync" protocol: + +* a node is connected to a random subset of all nodes that represents its peer set. Some nodes are correct and some + might be faulty. We don't make assumptions about ratio of faulty nodes, i.e., it is possible that all nodes in some + peer set are faulty. +* we assume that communication between correct nodes is synchronous, i.e., if a correct node `p` sends a message `m` to + a correct node `q` at time `t`, then `q` will receive message the latest at time `t+Delta` where `Delta` is a system + parameter that is known by network participants. `Delta` is normally chosen to be an order of magnitude higher than + the real communication delay (maximum) between correct nodes. Therefore if a correct node `p` sends a request message + to a correct node `q` at time `t` and there is no the corresponding reply at time `t + 2*Delta`, then `p` can assume + that `q` is faulty. Note that the network assumptions for the consensus reactor are different (we assume partially + synchronous model there). + +The requirements for the "fast sync" protocol are formally specified as follows: + +- `Correctness`: If a correct node `p` is connected to a correct node `q` for a long enough period of time, then `p` +- will eventually download all requested blocks from `q`. +- `Termination`: If a set of peers of a correct node `p` is stable (no new nodes are added to the peer set of `p`) for +- a long enough period of time, then protocol eventually terminates. +- `Fairness`: A correct node `p` sends requests for blocks to all peers from its peer set. + +As explained above, the `Executor` is responsible for sending and receiving messages that are part of the `fast-sync` +protocol. The following messages are exchanged as part of `fast-sync` protocol: + +``` go +type Message int +const ( + MessageUnknown Message = iota + MessageStatusRequest + MessageStatusResponse + MessageBlockRequest + MessageBlockResponse +) +``` +`MessageStatusRequest` is sent periodically to all peers as a request for a peer to provide its current height. It is +part of the `Peer Heartbeat` mechanism and a failure to respond timely to this message results in a peer being removed +from the peer set. Note that the `Peer Heartbeat` mechanism is used only while a peer is in `fast-sync` mode. We assume +here existence of a mechanism that gives node a possibility to inform its peers that it is in the `fast-sync` mode. + +``` go +type MessageStatusRequest struct { + SeqNum int64 // sequence number of the request +} +``` +`MessageStatusResponse` is sent as a response to `MessageStatusRequest` to inform requester about the peer current +height. + +``` go +type MessageStatusResponse struct { + SeqNum int64 // sequence number of the corresponding request + Height int64 // current peer height +} +``` + +`MessageBlockRequest` is used to make a request for a block and the corresponding commit certificate at a given height. + +``` go +type MessageBlockRequest struct { + Height int64 +} +``` + +`MessageBlockResponse` is a response for the corresponding block request. In addition to providing the block and the +corresponding commit certificate, it contains also a current peer height. + +``` go +type MessageBlockResponse struct { + Height int64 + Block Block + Commit Commit + PeerHeight int64 +} +``` + +In addition to sending and receiving messages, and `HeartBeat` mechanism, controller is also managing timeouts +that are triggered upon `Controller` request. `Controller` is then informed once a timeout expires. + +``` go +type TimeoutTrigger int +const ( + TimeoutUnknown TimeoutTrigger = iota + TimeoutResponseTrigger + TimeoutTerminationTrigger +) +``` + +The `Controller` can be modelled as a function with clearly defined inputs: + +* `State` - current state of the node. Contains data about connected peers and its behavior, pending requests, +* received blocks, etc. +* `Event` - significant events in the network. + +producing clear outputs: + +* `State` - updated state of the node, +* `MessageToSend` - signal what message to send and to which peer +* `TimeoutTrigger` - signal that timeout should be triggered. + + +We consider the following `Event` types: + +``` go +type Event int +const ( + EventUnknown Event = iota + EventStatusReport + EventBlockRequest + EventBlockResponse + EventRemovePeer + EventTimeoutResponse + EventTimeoutTermination +) +``` + +`EventStatusResponse` event is generated once `MessageStatusResponse` is received by the `Executor`. + +``` go +type EventStatusReport struct { + PeerID ID + Height int64 +} +``` + +`EventBlockRequest` event is generated once `MessageBlockRequest` is received by the `Executor`. + +``` go +type EventBlockRequest struct { + Height int64 + PeerID p2p.ID +} +``` +`EventBlockResponse` event is generated upon reception of `MessageBlockResponse` message by the `Executor`. + +``` go +type EventBlockResponse struct { + Height int64 + Block Block + Commit Commit + PeerID ID + PeerHeight int64 +} +``` +`EventRemovePeer` is generated by `Executor` to signal that the connection to a peer is closed due to peer misbehavior. + +``` go +type EventRemovePeer struct { + PeerID ID +} +``` +`EventTimeoutResponse` is generated by `Executor` to signal that a timeout triggered by `TimeoutResponseTrigger` has +expired. + +``` go +type EventTimeoutResponse struct { + PeerID ID + Height int64 +} +``` +`EventTimeoutTermination` is generated by `Executor` to signal that a timeout triggered by `TimeoutTerminationTrigger` +has expired. + +``` go +type EventTimeoutTermination struct { + Height int64 +} +``` + +`MessageToSend` is just a wrapper around `Message` type that contains id of the peer to which message should be sent. + +``` go +type MessageToSend struct { + PeerID ID + Message Message +} +``` + +The Controller state machine can be in two modes: `ModeFastSync` when +a node is trying to catch up with the network by downloading committed blocks, +and `ModeConsensus` in which it executes Tendermint consensus protocol. We +consider that `fast sync` mode terminates once the Controller switch to +`ModeConsensus`. + +``` go +type Mode int +const ( + ModeUnknown Mode = iota + ModeFastSync + ModeConsensus +) +``` +`Controller` is managing the following state: + +``` go +type ControllerState struct { + Height int64 // the first block that is not committed + Mode Mode // mode of operation + PeerMap map[ID]PeerStats // map of peer IDs to peer statistics + MaxRequestPending int64 // maximum height of the pending requests + FailedRequests []int64 // list of failed block requests + PendingRequestsNum int // total number of pending requests + Store []BlockInfo // contains list of downloaded blocks + Executor BlockExecutor // store, verify and executes blocks +} +``` + +`PeerStats` data structure keeps for every peer its current height and a list of pending requests for blocks. + +``` go +type PeerStats struct { + Height int64 + PendingRequest int64 // a request sent to this peer +} +``` + +`BlockInfo` data structure is used to store information (as part of block store) about downloaded blocks: from what peer + a block and the corresponding commit certificate are received. +``` go +type BlockInfo struct { + Block Block + Commit Commit + PeerID ID // a peer from which we received the corresponding Block and Commit +} +``` + +The `Controller` is initialized by providing an initial height (`startHeight`) from which it will start downloading +blocks from peers and the current state of the `BlockExecutor`. + +``` go +func NewControllerState(startHeight int64, executor BlockExecutor) ControllerState { + state = ControllerState {} + state.Height = startHeight + state.Mode = ModeFastSync + state.MaxRequestPending = startHeight - 1 + state.PendingRequestsNum = 0 + state.Executor = executor + initialize state.PeerMap, state.FailedRequests and state.Store to empty data structures + return state +} +``` + +The core protocol logic is given with the following function: + +``` go +func handleEvent(state ControllerState, event Event) (ControllerState, Message, TimeoutTrigger, Error) { + msg = nil + timeout = nil + error = nil + + switch state.Mode { + case ModeConsensus: + switch event := event.(type) { + case EventBlockRequest: + msg = createBlockResponseMessage(state, event) + return state, msg, timeout, error + default: + error = "Only respond to BlockRequests while in ModeConsensus!" + return state, msg, timeout, error + } + + case ModeFastSync: + switch event := event.(type) { + case EventBlockRequest: + msg = createBlockResponseMessage(state, event) + return state, msg, timeout, error + + case EventStatusResponse: + return handleEventStatusResponse(event, state) + + case EventRemovePeer: + return handleEventRemovePeer(event, state) + + case EventBlockResponse: + return handleEventBlockResponse(event, state) + + case EventResponseTimeout: + return handleEventResponseTimeout(event, state) + + case EventTerminationTimeout: + // Termination timeout is triggered in case of empty peer set and in case there are no pending requests. + // If this timeout expires and in the meantime no new peers are added or new pending requests are made + // then `fast-sync` mode terminates by switching to `ModeConsensus`. + // Note that termination timeout should be higher than the response timeout. + if state.Height == event.Height && state.PendingRequestsNum == 0 { state.State = ConsensusMode } + return state, msg, timeout, error + + default: + error = "Received unknown event type!" + return state, msg, timeout, error + } + } +} +``` + +``` go +func createBlockResponseMessage(state ControllerState, event BlockRequest) MessageToSend { + msgToSend = nil + if _, ok := state.PeerMap[event.PeerID]; !ok { peerStats = PeerStats{-1, -1} } + if state.Executor.ContainsBlockWithHeight(event.Height) && event.Height > peerStats.Height { + peerStats = event.Height + msg = BlockResponseMessage{ + Height: event.Height, + Block: state.Executor.getBlock(eventHeight), + Commit: state.Executor.getCommit(eventHeight), + PeerID: event.PeerID, + CurrentHeight: state.Height - 1, + } + msgToSend = MessageToSend { event.PeerID, msg } + } + state.PeerMap[event.PeerID] = peerStats + return msgToSend +} +``` + +``` go +func handleEventStatusResponse(event EventStatusResponse, state ControllerState) (ControllerState, MessageToSend, TimeoutTrigger, Error) { + if _, ok := state.PeerMap[event.PeerID]; !ok { + peerStats = PeerStats{ -1, -1 } + } else { + peerStats = state.PeerMap[event.PeerID] + } + + if event.Height > peerStats.Height { peerStats.Height = event.Height } + // if there are no pending requests for this peer, try to send him a request for block + if peerStats.PendingRequest == -1 { + msg = createBlockRequestMessages(state, event.PeerID, peerStats.Height) + // msg is nil if no request for block can be made to a peer at this point in time + if msg != nil { + peerStats.PendingRequests = msg.Height + state.PendingRequestsNum++ + // when a request for a block is sent to a peer, a response timeout is triggered. If no corresponding block is sent by the peer + // during response timeout period, then the peer is considered faulty and is removed from the peer set. + timeout = ResponseTimeoutTrigger{ msg.PeerID, msg.Height, PeerTimeout } + } else if state.PendingRequestsNum == 0 { + // if there are no pending requests and no new request can be placed to the peer, termination timeout is triggered. + // If termination timeout expires and we are still at the same height and there are no pending requests, the "fast-sync" + // mode is finished and we switch to `ModeConsensus`. + timeout = TerminationTimeoutTrigger{ state.Height, TerminationTimeout } + } + } + state.PeerMap[event.PeerID] = peerStats + return state, msg, timeout, error +} +``` + +``` go +func handleEventRemovePeer(event EventRemovePeer, state ControllerState) (ControllerState, MessageToSend, TimeoutTrigger, Error) { + if _, ok := state.PeerMap[event.PeerID]; ok { + pendingRequest = state.PeerMap[event.PeerID].PendingRequest + // if a peer is removed from the peer set, its pending request is declared failed and added to the `FailedRequests` list + // so it can be retried. + if pendingRequest != -1 { + add(state.FailedRequests, pendingRequest) + } + state.PendingRequestsNum-- + delete(state.PeerMap, event.PeerID) + // if the peer set is empty after removal of this peer then termination timeout is triggered. + if state.PeerMap.isEmpty() { + timeout = TerminationTimeoutTrigger{ state.Height, TerminationTimeout } + } + } else { error = "Removing unknown peer!" } + return state, msg, timeout, error +``` + +``` go +func handleEventBlockResponse(event EventBlockResponse, state ControllerState) (ControllerState, MessageToSend, TimeoutTrigger, Error) + if state.PeerMap[event.PeerID] { + peerStats = state.PeerMap[event.PeerID] + // when expected block arrives from a peer, it is added to the store so it can be verified and if correct executed after. + if peerStats.PendingRequest == event.Height { + peerStats.PendingRequest = -1 + state.PendingRequestsNum-- + if event.PeerHeight > peerStats.Height { peerStats.Height = event.PeerHeight } + state.Store[event.Height] = BlockInfo{ event.Block, event.Commit, event.PeerID } + // blocks are verified sequentially so adding a block to the store does not mean that it will be immediately verified + // as some of the previous blocks might be missing. + state = verifyBlocks(state) // it can lead to event.PeerID being removed from peer list + if _, ok := state.PeerMap[event.PeerID]; ok { + // we try to identify new request for a block that can be asked to the peer + msg = createBlockRequestMessage(state, event.PeerID, peerStats.Height) + if msg != nil { + peerStats.PendingRequests = msg.Height + state.PendingRequestsNum++ + // if request for block is made, response timeout is triggered + timeout = ResponseTimeoutTrigger{ msg.PeerID, msg.Height, PeerTimeout } + } else if state.PeerMap.isEmpty() || state.PendingRequestsNum == 0 { + // if the peer map is empty (the peer can be removed as block verification failed) or there are no pending requests + // termination timeout is triggered. + timeout = TerminationTimeoutTrigger{ state.Height, TerminationTimeout } + } + } + } else { error = "Received Block from wrong peer!" } + } else { error = "Received Block from unknown peer!" } + + state.PeerMap[event.PeerID] = peerStats + return state, msg, timeout, error +} +``` + +``` go +func handleEventResponseTimeout(event, state) { + if _, ok := state.PeerMap[event.PeerID]; ok { + peerStats = state.PeerMap[event.PeerID] + // if a response timeout expires and the peer hasn't delivered the block, the peer is removed from the peer list and + // the request is added to the `FailedRequests` so the block can be downloaded from other peer + if peerStats.PendingRequest == event.Height { + add(state.FailedRequests, pendingRequest) + delete(state.PeerMap, event.PeerID) + state.PendingRequestsNum-- + // if peer set is empty, then termination timeout is triggered + if state.PeerMap.isEmpty() { + timeout = TimeoutTrigger{ state.Height, TerminationTimeout } + } + } + } + return state, msg, timeout, error +} +``` + +``` go +func createBlockRequestMessage(state ControllerState, peerID ID, peerHeight int64) MessageToSend { + msg = nil + blockHeight = -1 + r = find request in state.FailedRequests such that r <= peerHeight // returns `nil` if there are no such request + // if there is a height in failed requests that can be downloaded from the peer send request to it + if r != nil { + blockNumber = r + delete(state.FailedRequests, r) + } else if state.MaxRequestPending < peerHeight { + // if height of the maximum pending request is smaller than peer height, then ask peer for next block + state.MaxRequestPending++ + blockHeight = state.MaxRequestPending // increment state.MaxRequestPending and then return the new value + } + + if blockHeight > -1 { msg = MessageToSend { peerID, MessageBlockRequest { blockHeight } } + return msg +} +``` + +``` go +func verifyBlocks(state State) State { + done = false + for !done { + block = state.Store[height] + if block != nil { + verified = verify block.Block using block.Commit // return `true` is verification succeed, 'false` otherwise + + if verified { + block.Execute() // executing block is costly operation so it might make sense executing asynchronously + state.Height++ + } else { + // if block verification failed, then it is added to `FailedRequests` and the peer is removed from the peer set + add(state.FailedRequests, height) + state.Store[height] = nil + if _, ok := state.PeerMap[block.PeerID]; ok { + pendingRequest = state.PeerMap[block.PeerID].PendingRequest + // if there is a pending request sent to the peer that is just to be removed from the peer set, add it to `FailedRequests` + if pendingRequest != -1 { + add(state.FailedRequests, pendingRequest) + state.PendingRequestsNum-- + } + delete(state.PeerMap, event.PeerID) + } + done = true + } + } else { done = true } + } + return state +} +``` + +In the proposed architecture `Controller` is not active task, i.e., it is being called by the `Executor`. Depending on +the return values returned by `Controller`,`Executor` will send a message to some peer (`msg` != nil), trigger a +timeout (`timeout` != nil) or deal with errors (`error` != nil). +In case a timeout is triggered, it will provide as an input to `Controller` the corresponding timeout event once +timeout expires. + + +## Status + +Draft. + +## Consequences + +### Positive + +- isolated implementation of the algorithm +- improved testability - simpler to prove correctness +- clearer separation of concerns - easier to reason + +### Negative + +### Neutral diff --git a/docs/architecture/img/bc-reactor-refactor.png b/docs/architecture/img/bc-reactor-refactor.png new file mode 100644 index 0000000000000000000000000000000000000000..4cd84a02f600154a1d59c34b1e7f2f09ea66167f GIT binary patch literal 8105 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 bZSC!R|IZ3f5A-kY-T~^$T1w>#Rw4fd6<|rR literal 0 HcmV?d00001 diff --git a/docs/architecture/img/bc-reactor.png b/docs/architecture/img/bc-reactor.png new file mode 100644 index 0000000000000000000000000000000000000000..f7fe0f8193d93ce7d0461e064acffc0426f38d55 GIT binary patch literal 44211 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 I?OV_O1Bz9-`2YX_ literal 0 HcmV?d00001 diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index cdbdf1fe2..f24e76d6d 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -42,7 +42,7 @@ fast_sync = true # - EXPERIMENTAL # - may be faster is some use-cases (random reads - indexer) # - use boltdb build tag (go build -tags boltdb) -db_backend = "leveldb" +db_backend = "goleveldb" # Database directory db_dir = "data" diff --git a/docs/tendermint-core/running-in-production.md b/docs/tendermint-core/running-in-production.md index 1ec792831..9cb21fc54 100644 --- a/docs/tendermint-core/running-in-production.md +++ b/docs/tendermint-core/running-in-production.md @@ -8,7 +8,7 @@ key-value database. Unfortunately, this implementation of LevelDB seems to suffe install the real C-implementation of LevelDB and compile Tendermint to use that using `make build_c`. See the [install instructions](../introduction/install.md) for details. -Tendermint keeps multiple distinct LevelDB databases in the `$TMROOT/data`: +Tendermint keeps multiple distinct databases in the `$TMROOT/data`: - `blockstore.db`: Keeps the entire blockchain - stores blocks, block commits, and block meta data, each indexed by height. Used to sync new From 86cf8ee3f96fbff39ebf79d58e186201a2bd2222 Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Fri, 10 May 2019 19:45:50 +0200 Subject: [PATCH 034/211] p2p: PeerBehaviour implementation (#3539) (#3552) * p2p: initial implementation of peer behaviour * [p2p] re-use newMockPeer * p2p: add inline docs for peer_behaviour interface * [p2p] Align PeerBehaviour interface (#3558) * [p2p] make switchedPeerHebaviour private * [p2p] make storePeerHebaviour private * [p2p] Add CHANGELOG_PENDING entry for PeerBehaviour * [p2p] Adjustment naming for PeerBehaviour * [p2p] Add coarse lock around storedPeerBehaviour * [p2p]: Fix non-pointer methods in storedPeerBehaviour + Structs with embeded locks must specify all methods with pointer receivers to avoid creating a copy of the embeded lock. * [p2p] Thorough refactoring based on comments in #3552 + Decouple PeerBehaviour interface from Peer by parametrizing methods with `p2p.ID` instead of `p2p.Peer` + Setter methods wrapped in a write lock + Getter methods wrapped in a read lock + Getter methods on storedPeerBehaviour now take a `p2p.ID` + Getter methods return a copy of underlying stored behaviours + Add doc strings to public types and methods * [p2p] make structs public * [p2p] Test empty StoredPeerBehaviour * [p2p] typo fix * [p2p] add TestStoredPeerBehaviourConcurrency + Add a test which uses StoredPeerBehaviour in multiple goroutines to ensure thread-safety. * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour_test.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * Update p2p/peer_behaviour.go Co-Authored-By: brapse * [p2p] field ordering convention * p2p: peer behaviour refactor + Change naming of reporting behaviour to `Report` + Remove the responsibility of distinguishing between the categories of good and bad behaviour and instead focus on reporting behaviour. * p2p: rename PeerReporter -> PeerBehaviourReporter --- CHANGELOG_PENDING.md | 2 + p2p/peer_behaviour.go | 92 +++++++++++++++++++++++++++++ p2p/peer_behaviour_test.go | 115 +++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+) create mode 100644 p2p/peer_behaviour.go create mode 100644 p2p/peer_behaviour_test.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 0067c8c15..fbb87eef1 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -31,6 +31,8 @@ - [node] \#2659 Add `node.Mempool()` method, which allows you to access mempool ### IMPROVEMENTS: +- [p2p] [\#3463](https://github.com/tendermint/tendermint/pull/3463) Do not log "Can't add peer's address to addrbook" error for a private peer +- [p2p] [\#3552](https://github.com/tendermint/tendermint/pull/3552) Add PeerBehaviour Interface (@brapse) - [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC - [cli] \#3585 Add option to not clear address book with unsafe reset (@climber73) - [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `-config=` option to `testnet` cmd (@gregdhill) diff --git a/p2p/peer_behaviour.go b/p2p/peer_behaviour.go new file mode 100644 index 000000000..8d27805c3 --- /dev/null +++ b/p2p/peer_behaviour.go @@ -0,0 +1,92 @@ +package p2p + +import ( + "errors" + "sync" +) + +// PeerBehaviour are types of reportable behaviours about peers. +type PeerBehaviour int + +const ( + PeerBehaviourBadMessage = iota + PeerBehaviourMessageOutOfOrder + PeerBehaviourVote + PeerBehaviourBlockPart +) + +// PeerBehaviourReporter provides an interface for reactors to report the behaviour +// of peers synchronously to other components. +type PeerBehaviourReporter interface { + Report(peerID ID, behaviour PeerBehaviour) error +} + +// SwitchPeerBehaviouReporter reports peer behaviour to an internal Switch +type SwitchPeerBehaviourReporter struct { + sw *Switch +} + +// Return a new SwitchPeerBehaviourReporter instance which wraps the Switch. +func NewSwitchPeerBehaviourReporter(sw *Switch) *SwitchPeerBehaviourReporter { + return &SwitchPeerBehaviourReporter{ + sw: sw, + } +} + +// Report reports the behaviour of a peer to the Switch +func (spbr *SwitchPeerBehaviourReporter) Report(peerID ID, behaviour PeerBehaviour) error { + peer := spbr.sw.Peers().Get(peerID) + if peer == nil { + return errors.New("Peer not found") + } + + switch behaviour { + case PeerBehaviourVote, PeerBehaviourBlockPart: + spbr.sw.MarkPeerAsGood(peer) + case PeerBehaviourBadMessage: + spbr.sw.StopPeerForError(peer, "Bad message") + case PeerBehaviourMessageOutOfOrder: + spbr.sw.StopPeerForError(peer, "Message out of order") + default: + return errors.New("Unknown behaviour") + } + + return nil +} + +// MockPeerBehaviourReporter serves a mock concrete implementation of the +// PeerBehaviourReporter interface used in reactor tests to ensure reactors +// report the correct behaviour in manufactured scenarios. +type MockPeerBehaviourReporter struct { + mtx sync.RWMutex + pb map[ID][]PeerBehaviour +} + +// NewMockPeerBehaviourReporter returns a PeerBehaviourReporter which records all reported +// behaviours in memory. +func NewMockPeerBehaviourReporter() *MockPeerBehaviourReporter { + return &MockPeerBehaviourReporter{ + pb: map[ID][]PeerBehaviour{}, + } +} + +// Report stores the PeerBehaviour produced by the peer identified by peerID. +func (mpbr *MockPeerBehaviourReporter) Report(peerID ID, behaviour PeerBehaviour) { + mpbr.mtx.Lock() + defer mpbr.mtx.Unlock() + mpbr.pb[peerID] = append(mpbr.pb[peerID], behaviour) +} + +// GetBehaviours returns all behaviours reported on the peer identified by peerID. +func (mpbr *MockPeerBehaviourReporter) GetBehaviours(peerID ID) []PeerBehaviour { + mpbr.mtx.RLock() + defer mpbr.mtx.RUnlock() + if items, ok := mpbr.pb[peerID]; ok { + result := make([]PeerBehaviour, len(items)) + copy(result, items) + + return result + } else { + return []PeerBehaviour{} + } +} diff --git a/p2p/peer_behaviour_test.go b/p2p/peer_behaviour_test.go new file mode 100644 index 000000000..03ea4969f --- /dev/null +++ b/p2p/peer_behaviour_test.go @@ -0,0 +1,115 @@ +package p2p + +import ( + "net" + "sync" + "testing" +) + +// TestMockPeerBehaviour tests the MockPeerBehaviour' ability to store reported +// peer behaviour in memory indexed by the peerID +func TestMockPeerBehaviourReporter(t *testing.T) { + peer := newMockPeer(net.IP{127, 0, 0, 1}) + pr := NewMockPeerBehaviourReporter() + + behaviours := pr.GetBehaviours(peer.ID()) + if len(behaviours) != 0 { + t.Errorf("Expected to have no behaviours reported") + } + + pr.Report(peer.ID(), PeerBehaviourBadMessage) + behaviours = pr.GetBehaviours(peer.ID()) + if len(behaviours) != 1 { + t.Errorf("Expected the peer have one reported behaviour") + } + + if behaviours[0] != PeerBehaviourBadMessage { + t.Errorf("Expected PeerBehaviourBadMessage to have been reported") + } +} + +type scriptedBehaviours struct { + PeerID ID + Behaviours []PeerBehaviour +} + +type scriptItem struct { + PeerID ID + Behaviour PeerBehaviour +} + +func equalBehaviours(a []PeerBehaviour, b []PeerBehaviour) bool { + if len(a) != len(b) { + return false + } + same := make([]PeerBehaviour, len(a)) + + for i, aBehaviour := range a { + for _, bBehaviour := range b { + if aBehaviour == bBehaviour { + same[i] = aBehaviour + } + } + } + + return len(same) == len(a) +} + +// TestPeerBehaviourConcurrency constructs a scenario in which +// multiple goroutines are using the same MockPeerBehaviourReporter instance. +// This test reproduces the conditions in which MockPeerBehaviourReporter will +// be used within a Reactor Receive method tests to ensure thread safety. +func TestMockPeerBehaviourReporterConcurrency(t *testing.T) { + behaviourScript := []scriptedBehaviours{ + {"1", []PeerBehaviour{PeerBehaviourVote}}, + {"2", []PeerBehaviour{PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote}}, + {"3", []PeerBehaviour{PeerBehaviourBlockPart, PeerBehaviourVote, PeerBehaviourBlockPart, PeerBehaviourVote}}, + {"4", []PeerBehaviour{PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote}}, + {"5", []PeerBehaviour{PeerBehaviourBlockPart, PeerBehaviourVote, PeerBehaviourBlockPart, PeerBehaviourVote}}, + } + + var receiveWg sync.WaitGroup + pr := NewMockPeerBehaviourReporter() + scriptItems := make(chan scriptItem) + done := make(chan int) + numConsumers := 3 + for i := 0; i < numConsumers; i++ { + receiveWg.Add(1) + go func() { + defer receiveWg.Done() + for { + select { + case pb := <-scriptItems: + pr.Report(pb.PeerID, pb.Behaviour) + case <-done: + return + } + } + }() + } + + var sendingWg sync.WaitGroup + sendingWg.Add(1) + go func() { + defer sendingWg.Done() + for _, item := range behaviourScript { + for _, reason := range item.Behaviours { + scriptItems <- scriptItem{item.PeerID, reason} + } + } + }() + + sendingWg.Wait() + + for i := 0; i < numConsumers; i++ { + done <- 1 + } + + receiveWg.Wait() + + for _, items := range behaviourScript { + if !equalBehaviours(pr.GetBehaviours(items.PeerID), items.Behaviours) { + t.Errorf("Expected peer %s to have behaved \n", items.PeerID) + } + } +} From c21b4fcc9309d199afcb0f7f1e1dd5b0f093490f Mon Sep 17 00:00:00 2001 From: yutianwu Date: Tue, 21 May 2019 14:12:40 +0800 Subject: [PATCH 035/211] fix bug of proxy --- CHANGELOG_PENDING.md | 1 + lite/proxy/proxy.go | 103 +++++++++++++++++++++++++++++++++++++------ 2 files changed, 90 insertions(+), 14 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index fbb87eef1..fe946a301 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -59,3 +59,4 @@ * `Mempool#Update` and `BlockExecutor#Commit` now accept `[]*abci.ResponseDeliverTx` - list of `DeliverTx` responses, which should match `block.Txs` +- [rpc] \#3669 Handlers in proxy routes miss context parameter diff --git a/lite/proxy/proxy.go b/lite/proxy/proxy.go index d3c16d4a1..2b327c61f 100644 --- a/lite/proxy/proxy.go +++ b/lite/proxy/proxy.go @@ -4,13 +4,16 @@ import ( "context" "net/http" - amino "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/go-amino" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" rpcclient "github.com/tendermint/tendermint/rpc/client" "github.com/tendermint/tendermint/rpc/core" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpcserver "github.com/tendermint/tendermint/rpc/lib/server" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" + "github.com/tendermint/tendermint/types" ) const ( @@ -66,21 +69,93 @@ func RPCRoutes(c rpcclient.Client) map[string]*rpcserver.RPCFunc { "unsubscribe_all": rpcserver.NewWSRPCFunc(c.(Wrapper).UnsubscribeAllWS, ""), // info API - "status": rpcserver.NewRPCFunc(c.Status, ""), - "blockchain": rpcserver.NewRPCFunc(c.BlockchainInfo, "minHeight,maxHeight"), - "genesis": rpcserver.NewRPCFunc(c.Genesis, ""), - "block": rpcserver.NewRPCFunc(c.Block, "height"), - "commit": rpcserver.NewRPCFunc(c.Commit, "height"), - "tx": rpcserver.NewRPCFunc(c.Tx, "hash,prove"), - "validators": rpcserver.NewRPCFunc(c.Validators, "height"), + "status": rpcserver.NewRPCFunc(makeStatusFunc(c), ""), + "blockchain": rpcserver.NewRPCFunc(makeBlockchainInfoFunc(c), "minHeight,maxHeight"), + "genesis": rpcserver.NewRPCFunc(makeGenesisFunc(c), ""), + "block": rpcserver.NewRPCFunc(makeBlockFunc(c), "height"), + "commit": rpcserver.NewRPCFunc(makeCommitFunc(c), "height"), + "tx": rpcserver.NewRPCFunc(makeTxFunc(c), "hash,prove"), + "validators": rpcserver.NewRPCFunc(makeValidatorsFunc(c), "height"), // broadcast API - "broadcast_tx_commit": rpcserver.NewRPCFunc(c.BroadcastTxCommit, "tx"), - "broadcast_tx_sync": rpcserver.NewRPCFunc(c.BroadcastTxSync, "tx"), - "broadcast_tx_async": rpcserver.NewRPCFunc(c.BroadcastTxAsync, "tx"), + "broadcast_tx_commit": rpcserver.NewRPCFunc(makeBroadcastTxCommitFunc(c), "tx"), + "broadcast_tx_sync": rpcserver.NewRPCFunc(makeBroadcastTxSyncFunc(c), "tx"), + "broadcast_tx_async": rpcserver.NewRPCFunc(makeBroadcastTxAsyncFunc(c), "tx"), // abci API - "abci_query": rpcserver.NewRPCFunc(c.ABCIQuery, "path,data"), - "abci_info": rpcserver.NewRPCFunc(c.ABCIInfo, ""), + "abci_query": rpcserver.NewRPCFunc(makeABCIQueryFunc(c), "path,data"), + "abci_info": rpcserver.NewRPCFunc(makeABCIInfoFunc(c), ""), + } +} + +func makeStatusFunc(c rpcclient.Client) func(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { + return func(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { + return c.Status() + } +} + +func makeBlockchainInfoFunc(c rpcclient.Client) func(ctx *rpctypes.Context, minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { + return func(ctx *rpctypes.Context, minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { + return c.BlockchainInfo(minHeight, maxHeight) + } +} + +func makeGenesisFunc(c rpcclient.Client) func(ctx *rpctypes.Context) (*ctypes.ResultGenesis, error) { + return func(ctx *rpctypes.Context) (*ctypes.ResultGenesis, error) { + return c.Genesis() + } +} + +func makeBlockFunc(c rpcclient.Client) func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultBlock, error) { + return func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultBlock, error) { + return c.Block(height) + } +} + +func makeCommitFunc(c rpcclient.Client) func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultCommit, error) { + return func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultCommit, error) { + return c.Commit(height) + } +} + +func makeTxFunc(c rpcclient.Client) func(ctx *rpctypes.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) { + return func(ctx *rpctypes.Context, hash []byte, prove bool) (*ctypes.ResultTx, error) { + return c.Tx(hash, prove) + } +} + +func makeValidatorsFunc(c rpcclient.Client) func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultValidators, error) { + return func(ctx *rpctypes.Context, height *int64) (*ctypes.ResultValidators, error) { + return c.Validators(height) + } +} + +func makeBroadcastTxCommitFunc(c rpcclient.Client) func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + return func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + return c.BroadcastTxCommit(tx) + } +} + +func makeBroadcastTxSyncFunc(c rpcclient.Client) func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return c.BroadcastTxSync(tx) + } +} + +func makeBroadcastTxAsyncFunc(c rpcclient.Client) func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return func(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return c.BroadcastTxAsync(tx) + } +} + +func makeABCIQueryFunc(c rpcclient.Client) func(ctx *rpctypes.Context, path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { + return func(ctx *rpctypes.Context, path string, data cmn.HexBytes) (*ctypes.ResultABCIQuery, error) { + return c.ABCIQuery(path, data) + } +} + +func makeABCIInfoFunc(c rpcclient.Client) func(ctx *rpctypes.Context) (*ctypes.ResultABCIInfo, error) { + return func(ctx *rpctypes.Context) (*ctypes.ResultABCIInfo, error) { + return c.ABCIInfo() } } From fcce9ed4db5fd3334230b3645c5198f31fa686e9 Mon Sep 17 00:00:00 2001 From: yutianwu Date: Tue, 21 May 2019 14:23:50 +0800 Subject: [PATCH 036/211] fix lint --- lite/proxy/proxy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lite/proxy/proxy.go b/lite/proxy/proxy.go index 2b327c61f..80343a531 100644 --- a/lite/proxy/proxy.go +++ b/lite/proxy/proxy.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - "github.com/tendermint/go-amino" + amino "github.com/tendermint/go-amino" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" From f1f243d74965c556a119e7c9326af152daf5f0f9 Mon Sep 17 00:00:00 2001 From: Roman Shtylman Date: Tue, 21 May 2019 14:05:56 -0700 Subject: [PATCH 037/211] p2p/pex: consult seeds in crawlPeersRoutine (#3647) * p2p/pex: consult seeds in crawlPeersRoutine This changeset alters the startup behavior for crawlPeersRoutine. Previously the routine would crawl a random selection of peers on startup. For a new seed node, there are no peers. As a result, new seed nodes are unable to bootstrap themselves with a list of peers until another node with a list of peers connects to the seed. If this node relies on the seed node for peers, then the two will not discover more peers. This changeset makes the startup behavior for crawlPeersRoutine connect to any seed nodes. Upon connecting, a request for peers will be sent to the seed node thus helping bootstrap our seed node. * p2p/pex: Adjust error message for no peers Co-Authored-By: Ethan Buchman --- p2p/pex/pex_reactor.go | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index 957dbf802..551ed903d 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -145,7 +145,7 @@ func (r *PEXReactor) OnStart() error { if err != nil { return err } else if numOnline == 0 && r.book.Empty() { - return errors.New("Address book is empty, and could not connect to any seed nodes") + return errors.New("Address book is empty and couldn't resolve any seed nodes") } r.seedAddrs = seedAddrs @@ -573,7 +573,7 @@ func (r *PEXReactor) checkSeeds() (numOnline int, netAddrs []*p2p.NetAddress, er return 0, nil, errors.Wrap(e, "seed node configuration has error") } } - return + return numOnline, netAddrs, nil } // randomly dial seeds until we connect to one or exhaust them @@ -608,8 +608,13 @@ func (r *PEXReactor) AttemptsToDial(addr *p2p.NetAddress) int { // Seed/Crawler Mode causes this node to quickly disconnect // from peers, except other seed nodes. func (r *PEXReactor) crawlPeersRoutine() { - // Do an initial crawl - r.crawlPeers(r.book.GetSelection()) + // If we have any seed nodes, consult them first + if len(r.seedAddrs) > 0 { + r.dialSeeds() + } else { + // Do an initial crawl + r.crawlPeers(r.book.GetSelection()) + } // Fire periodically ticker := time.NewTicker(crawlPeerPeriod) From cfd42be0fe39204b6fa3f799ee40b964e27ca0f6 Mon Sep 17 00:00:00 2001 From: Aleksandr Bezobchuk Date: Wed, 22 May 2019 20:02:00 -0400 Subject: [PATCH 038/211] Improve error and tests --- crypto/multisig/multisignature.go | 15 +++-- crypto/multisig/threshold_pubkey_test.go | 74 ++++++++++++++++++------ 2 files changed, 67 insertions(+), 22 deletions(-) diff --git a/crypto/multisig/multisignature.go b/crypto/multisig/multisignature.go index 0d1796890..1e3bef4e1 100644 --- a/crypto/multisig/multisignature.go +++ b/crypto/multisig/multisignature.go @@ -1,7 +1,8 @@ package multisig import ( - "errors" + "fmt" + "strings" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/multisig/bitarray" @@ -53,13 +54,19 @@ func (mSig *Multisignature) AddSignature(sig []byte, index int) { mSig.Sigs[newSigIndex] = sig } -// AddSignatureFromPubKey adds a signature to the multisig, -// at the index in keys corresponding to the provided pubkey. +// AddSignatureFromPubKey adds a signature to the multisig, at the index in +// keys corresponding to the provided pubkey. func (mSig *Multisignature) AddSignatureFromPubKey(sig []byte, pubkey crypto.PubKey, keys []crypto.PubKey) error { index := getIndex(pubkey, keys) if index == -1 { - return errors.New("provided key didn't exist in pubkeys") + keysStr := make([]string, len(keys)) + for i, k := range keys { + keysStr[i] = fmt.Sprintf("%X", k.Bytes()) + } + + return fmt.Errorf("provided key %X doesn't exist in pubkeys: \n%s", pubkey.Bytes(), strings.Join(keysStr, "\n")) } + mSig.AddSignature(sig, index) return nil } diff --git a/crypto/multisig/threshold_pubkey_test.go b/crypto/multisig/threshold_pubkey_test.go index 2d2632abd..d1d7e803c 100644 --- a/crypto/multisig/threshold_pubkey_test.go +++ b/crypto/multisig/threshold_pubkey_test.go @@ -36,30 +36,68 @@ func TestThresholdMultisigValidCases(t *testing.T) { for tcIndex, tc := range cases { multisigKey := NewPubKeyMultisigThreshold(tc.k, tc.pubkeys) multisignature := NewMultisig(len(tc.pubkeys)) + for i := 0; i < tc.k-1; i++ { signingIndex := tc.signingIndices[i] - multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) - require.False(t, multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), - "multisig passed when i < k, tc %d, i %d", tcIndex, i) - multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) - require.Equal(t, i+1, len(multisignature.Sigs), - "adding a signature for the same pubkey twice increased signature count by 2, tc %d", tcIndex) + require.NoError( + t, + multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys), + ) + require.False( + t, + multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig passed when i < k, tc %d, i %d", tcIndex, i, + ) + require.NoError( + t, + multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys), + ) + require.Equal( + t, + i+1, + len(multisignature.Sigs), + "adding a signature for the same pubkey twice increased signature count by 2, tc %d", tcIndex, + ) } - require.False(t, multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), - "multisig passed with k - 1 sigs, tc %d", tcIndex) - multisignature.AddSignatureFromPubKey(tc.signatures[tc.signingIndices[tc.k]], tc.pubkeys[tc.signingIndices[tc.k]], tc.pubkeys) - require.True(t, multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), - "multisig failed after k good signatures, tc %d", tcIndex) + + require.False( + t, + multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig passed with k - 1 sigs, tc %d", tcIndex, + ) + require.NoError( + t, + multisignature.AddSignatureFromPubKey(tc.signatures[tc.signingIndices[tc.k]], tc.pubkeys[tc.signingIndices[tc.k]], tc.pubkeys), + ) + require.True( + t, + multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig failed after k good signatures, tc %d", tcIndex, + ) + for i := tc.k + 1; i < len(tc.signingIndices); i++ { signingIndex := tc.signingIndices[i] - multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) - require.Equal(t, tc.passAfterKSignatures[i-tc.k-1], - multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), - "multisig didn't verify as expected after k sigs, tc %d, i %d", tcIndex, i) - multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys) - require.Equal(t, i+1, len(multisignature.Sigs), - "adding a signature for the same pubkey twice increased signature count by 2, tc %d", tcIndex) + require.NoError( + t, + multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys), + ) + require.Equal( + t, + tc.passAfterKSignatures[i-tc.k-1], + multisigKey.VerifyBytes(tc.msg, multisignature.Marshal()), + "multisig didn't verify as expected after k sigs, tc %d, i %d", tcIndex, i, + ) + require.NoError( + t, + multisignature.AddSignatureFromPubKey(tc.signatures[signingIndex], tc.pubkeys[signingIndex], tc.pubkeys), + ) + require.Equal( + t, + i+1, + len(multisignature.Sigs), + "adding a signature for the same pubkey twice increased signature count by 2, tc %d", tcIndex, + ) } } } From 3ef9e453b71fb20c8cb173be4b3ee2b4ef8431c1 Mon Sep 17 00:00:00 2001 From: yutianwu Date: Thu, 23 May 2019 14:26:11 +0800 Subject: [PATCH 039/211] update change log --- CHANGELOG_PENDING.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index fe946a301..709065f78 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -59,4 +59,5 @@ * `Mempool#Update` and `BlockExecutor#Commit` now accept `[]*abci.ResponseDeliverTx` - list of `DeliverTx` responses, which should match `block.Txs` -- [rpc] \#3669 Handlers in proxy routes miss context parameter +- [lite] \#3669 Add context parameter to RPC Handlers in proxy routes (@yutianwu) + From 5997e75c84f643a65cc411331e1f47201ebb5772 Mon Sep 17 00:00:00 2001 From: Carlos Flores Date: Thu, 23 May 2019 09:56:57 -0700 Subject: [PATCH 040/211] fix integration script (#3667) --- CHANGELOG_PENDING.md | 2 +- docs/networks/terraform-and-ansible.md | 16 +++++++++------- networks/remote/integration.sh | 6 ++++-- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 709065f78..8c89e0130 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -59,5 +59,5 @@ * `Mempool#Update` and `BlockExecutor#Commit` now accept `[]*abci.ResponseDeliverTx` - list of `DeliverTx` responses, which should match `block.Txs` +- [networks] fixes ansible integration script (@carlosflrs) - [lite] \#3669 Add context parameter to RPC Handlers in proxy routes (@yutianwu) - diff --git a/docs/networks/terraform-and-ansible.md b/docs/networks/terraform-and-ansible.md index c08ade17a..122591be0 100644 --- a/docs/networks/terraform-and-ansible.md +++ b/docs/networks/terraform-and-ansible.md @@ -62,16 +62,18 @@ There are several roles that are self-explanatory: First, we configure our droplets by specifying the paths for tendermint (`BINARY`) and the node files (`CONFIGDIR`). The latter expects any number of directories named `node0, node1, ...` and so on (equal to the -number of droplets created). For this example, we use pre-created files -from [this -directory](https://github.com/tendermint/tendermint/tree/master/docs/examples). -To create your own files, use either the `tendermint testnet` command or -review [manual deployments](./deploy-testnets.md). +number of droplets created). -Here's the command to run: +To create the node files run: ``` -ansible-playbook -i inventory/digital_ocean.py -l sentrynet config.yml -e BINARY=$GOPATH/src/github.com/tendermint/tendermint/build/tendermint -e CONFIGDIR=$GOPATH/src/github.com/tendermint/tendermint/docs/examples +tendermint testnet +``` + +Then, to configure our droplets run: + +``` +ansible-playbook -i inventory/digital_ocean.py -l sentrynet config.yml -e BINARY=$GOPATH/src/github.com/tendermint/tendermint/build/tendermint -e CONFIGDIR=$GOPATH/src/github.com/tendermint/tendermint/networks/remote/ansible/mytestnet ``` Voila! All your droplets now have the `tendermint` binary and required diff --git a/networks/remote/integration.sh b/networks/remote/integration.sh index 8150aad48..fb1b0b327 100644 --- a/networks/remote/integration.sh +++ b/networks/remote/integration.sh @@ -30,7 +30,6 @@ go get $REPO cd $GOPATH/src/$REPO ## build -git checkout zach/ansible make get_tools make get_vendor_deps make build @@ -84,8 +83,11 @@ ip3=$(strip $ip3) # all the ansible commands are also directory specific cd $GOPATH/src/github.com/tendermint/tendermint/networks/remote/ansible +# create config dirs +tendermint testnet + ansible-playbook -i inventory/digital_ocean.py -l sentrynet install.yml -ansible-playbook -i inventory/digital_ocean.py -l sentrynet config.yml -e BINARY=$GOPATH/src/github.com/tendermint/tendermint/build/tendermint -e CONFIGDIR=$GOPATH/src/github.com/tendermint/tendermint/docs/examples +ansible-playbook -i inventory/digital_ocean.py -l sentrynet config.yml -e BINARY=$GOPATH/src/github.com/tendermint/tendermint/build/tendermint -e CONFIGDIR=$GOPATH/src/github.com/tendermint/tendermint/networks/remote/ansible/mytestnet sleep 10 From bcf10d5bae37cd05373bc37c84e6baf818bf0e87 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 28 May 2019 03:39:58 +0900 Subject: [PATCH 041/211] p2p: peer state init too late and pex message too soon (#3634) * fix peer state init to late Peer does not have a state yet. We set it in AddPeer. We need an new interface before mconnection is started * pex message to soon fix reconnection pex send too fast, error is caused lastReceivedRequests is still not deleted when a peer reconnected * add test case for initpeer * add prove case * remove potentially infinite loop * Update consensus/reactor.go Co-Authored-By: guagualvcha * Update consensus/reactor_test.go Co-Authored-By: guagualvcha * document Reactor interface better * refactor TestReactorReceiveDoesNotPanicIfAddPeerHasntBeenCalledYet * fix merge conflicts * blockchain: remove peer's ID from the pool in InitPeer Refs #3338 * pex: resetPeersRequestsInfo both upon InitPeer and RemovePeer * ensure RemovePeer is always called before InitPeer by removing the peer from the switch last (after we've stopped it and removed from all reactors) * add some comments for ConsensusReactor#InitPeer * fix pex reactor * format code * fix spelling * update changelog * remove unused methods * do not clear lastReceivedRequests upon error only in RemovePeer * call InitPeer before we start the peer! * add a comment to InitPeer * write a test * use waitUntilSwitchHasAtLeastNPeers func * bring back timeouts * Test to ensure Receive panics if InitPeer has not been called --- CHANGELOG_PENDING.md | 5 +++ consensus/byzantine_test.go | 1 + consensus/reactor.go | 20 +++++++---- consensus/reactor_test.go | 44 +++++++++++++++++++++++++ evidence/reactor.go | 5 --- p2p/base_reactor.go | 24 ++++++++++++-- p2p/pex/pex_reactor.go | 24 +++++++------- p2p/pex/pex_reactor_test.go | 4 +-- p2p/switch.go | 17 ++++++++-- p2p/switch_test.go | 66 +++++++++++++++++++++++++++++++++++++ p2p/test_util.go | 2 +- 11 files changed, 180 insertions(+), 32 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8c89e0130..a25afbb19 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -22,6 +22,7 @@ * `MockMempool` moved to top-level mock package and renamed to `Mempool` - [libs/common] Removed `PanicSanity`, `PanicCrisis`, `PanicConsensus` and `PanicQ` - [node] Moved `GenesisDocProvider` and `DefaultGenesisDocProviderFunc` to state package +- [p2p] \#3346 Reactor#InitPeer method is added to Reactor interface * Blockchain Protocol @@ -50,6 +51,10 @@ - [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode to 16 (as a result, the node will stop retrying after a 35 hours time window) - [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests +- [consensus] \#3346 Create a peer state in consensus reactor before the peer + is started (@guagualvcha) +- [p2p] \#3338 Ensure RemovePeer is always called before InitPeer (upon a peer + reconnecting to our node) - [p2p] \#3362 make persistent prop independent of conn direction * `Switch#DialPeersAsync` now only takes a list of peers * `Switch#DialPeerWithAddress` now only takes an address diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index d181fb1a6..c2eb114dc 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -270,3 +270,4 @@ func (br *ByzantineReactor) RemovePeer(peer p2p.Peer, reason interface{}) { func (br *ByzantineReactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) { br.reactor.Receive(chID, peer, msgBytes) } +func (br *ByzantineReactor) InitPeer(peer p2p.Peer) p2p.Peer { return peer } diff --git a/consensus/reactor.go b/consensus/reactor.go index 937a12351..36e948f6d 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -155,16 +155,24 @@ func (conR *ConsensusReactor) GetChannels() []*p2p.ChannelDescriptor { } } -// AddPeer implements Reactor +// InitPeer implements Reactor by creating a state for the peer. +func (conR *ConsensusReactor) InitPeer(peer p2p.Peer) p2p.Peer { + peerState := NewPeerState(peer).SetLogger(conR.Logger) + peer.Set(types.PeerStateKey, peerState) + return peer +} + +// AddPeer implements Reactor by spawning multiple gossiping goroutines for the +// peer. func (conR *ConsensusReactor) AddPeer(peer p2p.Peer) { if !conR.IsRunning() { return } - // Create peerState for peer - peerState := NewPeerState(peer).SetLogger(conR.Logger) - peer.Set(types.PeerStateKey, peerState) - + peerState, ok := peer.Get(types.PeerStateKey).(*PeerState) + if !ok { + panic(fmt.Sprintf("peer %v has no state", peer)) + } // Begin routines for this peer. go conR.gossipDataRoutine(peer, peerState) go conR.gossipVotesRoutine(peer, peerState) @@ -177,7 +185,7 @@ func (conR *ConsensusReactor) AddPeer(peer p2p.Peer) { } } -// RemovePeer implements Reactor +// RemovePeer is a noop. func (conR *ConsensusReactor) RemovePeer(peer p2p.Peer, reason interface{}) { if !conR.IsRunning() { return diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index 9440b50c3..b237da6b5 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -23,6 +23,7 @@ import ( "github.com/tendermint/tendermint/libs/log" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p/mock" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -243,6 +244,49 @@ func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) { }, css) } +func TestReactorReceiveDoesNotPanicIfAddPeerHasntBeenCalledYet(t *testing.T) { + N := 1 + css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) + defer cleanup() + reactors, _, eventBuses := startConsensusNet(t, css, N) + defer stopConsensusNet(log.TestingLogger(), reactors, eventBuses) + + var ( + reactor = reactors[0] + peer = mock.NewPeer(nil) + msg = cdc.MustMarshalBinaryBare(&HasVoteMessage{Height: 1, Round: 1, Index: 1, Type: types.PrevoteType}) + ) + + reactor.InitPeer(peer) + + // simulate switch calling Receive before AddPeer + assert.NotPanics(t, func() { + reactor.Receive(StateChannel, peer, msg) + reactor.AddPeer(peer) + }) +} + +func TestReactorReceivePanicsIfInitPeerHasntBeenCalledYet(t *testing.T) { + N := 1 + css, cleanup := randConsensusNet(N, "consensus_reactor_test", newMockTickerFunc(true), newCounter) + defer cleanup() + reactors, _, eventBuses := startConsensusNet(t, css, N) + defer stopConsensusNet(log.TestingLogger(), reactors, eventBuses) + + var ( + reactor = reactors[0] + peer = mock.NewPeer(nil) + msg = cdc.MustMarshalBinaryBare(&HasVoteMessage{Height: 1, Round: 1, Index: 1, Type: types.PrevoteType}) + ) + + // we should call InitPeer here + + // simulate switch calling Receive before AddPeer + assert.Panics(t, func() { + reactor.Receive(StateChannel, peer, msg) + }) +} + // Test we record stats about votes and block parts from other peers. func TestReactorRecordsVotesAndBlockParts(t *testing.T) { N := 4 diff --git a/evidence/reactor.go b/evidence/reactor.go index bbbab3e96..76ea270d9 100644 --- a/evidence/reactor.go +++ b/evidence/reactor.go @@ -60,11 +60,6 @@ func (evR *EvidenceReactor) AddPeer(peer p2p.Peer) { go evR.broadcastEvidenceRoutine(peer) } -// RemovePeer implements Reactor. -func (evR *EvidenceReactor) RemovePeer(peer p2p.Peer, reason interface{}) { - // nothing to do -} - // Receive implements Reactor. // It adds any received evidence to the evpool. func (evR *EvidenceReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { diff --git a/p2p/base_reactor.go b/p2p/base_reactor.go index be65d2f14..3bccabd64 100644 --- a/p2p/base_reactor.go +++ b/p2p/base_reactor.go @@ -5,23 +5,40 @@ import ( "github.com/tendermint/tendermint/p2p/conn" ) +// Reactor is responsible for handling incoming messages on one or more +// Channel. Switch calls GetChannels when reactor is added to it. When a new +// peer joins our node, InitPeer and AddPeer are called. RemovePeer is called +// when the peer is stopped. Receive is called when a message is received on a +// channel associated with this reactor. +// +// Peer#Send or Peer#TrySend should be used to send the message to a peer. type Reactor interface { cmn.Service // Start, Stop // SetSwitch allows setting a switch. SetSwitch(*Switch) - // GetChannels returns the list of channel descriptors. + // GetChannels returns the list of MConnection.ChannelDescriptor. Make sure + // that each ID is unique across all the reactors added to the switch. GetChannels() []*conn.ChannelDescriptor - // AddPeer is called by the switch when a new peer is added. + // InitPeer is called by the switch before the peer is started. Use it to + // initialize data for the peer (e.g. peer state). + // + // NOTE: The switch won't call AddPeer nor RemovePeer if it fails to start + // the peer. Do not store any data associated with the peer in the reactor + // itself unless you don't want to have a state, which is never cleaned up. + InitPeer(peer Peer) Peer + + // AddPeer is called by the switch after the peer is added and successfully + // started. Use it to start goroutines communicating with the peer. AddPeer(peer Peer) // RemovePeer is called by the switch when the peer is stopped (due to error // or other reason). RemovePeer(peer Peer, reason interface{}) - // Receive is called when msgBytes is received from peer. + // Receive is called by the switch when msgBytes is received from the peer. // // NOTE reactor can not keep msgBytes around after Receive completes without // copying. @@ -51,3 +68,4 @@ func (*BaseReactor) GetChannels() []*conn.ChannelDescriptor { return nil func (*BaseReactor) AddPeer(peer Peer) {} func (*BaseReactor) RemovePeer(peer Peer, reason interface{}) {} func (*BaseReactor) Receive(chID byte, peer Peer, msgBytes []byte) {} +func (*BaseReactor) InitPeer(peer Peer) Peer { return peer } diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index 551ed903d..eabbc4d61 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -204,6 +204,13 @@ func (r *PEXReactor) AddPeer(p Peer) { } } +// RemovePeer implements Reactor by resetting peer's requests info. +func (r *PEXReactor) RemovePeer(p Peer, reason interface{}) { + id := string(p.ID()) + r.requestsSent.Delete(id) + r.lastReceivedRequests.Delete(id) +} + func (r *PEXReactor) logErrAddrBook(err error) { if err != nil { switch err.(type) { @@ -216,13 +223,6 @@ func (r *PEXReactor) logErrAddrBook(err error) { } } -// RemovePeer implements Reactor. -func (r *PEXReactor) RemovePeer(p Peer, reason interface{}) { - id := string(p.ID()) - r.requestsSent.Delete(id) - r.lastReceivedRequests.Delete(id) -} - // Receive implements Reactor by handling incoming PEX messages. func (r *PEXReactor) Receive(chID byte, src Peer, msgBytes []byte) { msg, err := decodeMsg(msgBytes) @@ -303,7 +303,7 @@ func (r *PEXReactor) receiveRequest(src Peer) error { now := time.Now() minInterval := r.minReceiveRequestInterval() if now.Sub(lastReceived) < minInterval { - return fmt.Errorf("Peer (%v) sent next PEX request too soon. lastReceived: %v, now: %v, minInterval: %v. Disconnecting", + return fmt.Errorf("peer (%v) sent next PEX request too soon. lastReceived: %v, now: %v, minInterval: %v. Disconnecting", src.ID(), lastReceived, now, @@ -314,14 +314,14 @@ func (r *PEXReactor) receiveRequest(src Peer) error { return nil } -// RequestAddrs asks peer for more addresses if we do not already -// have a request out for this peer. +// RequestAddrs asks peer for more addresses if we do not already have a +// request out for this peer. func (r *PEXReactor) RequestAddrs(p Peer) { - r.Logger.Debug("Request addrs", "from", p) id := string(p.ID()) if r.requestsSent.Has(id) { return } + r.Logger.Debug("Request addrs", "from", p) r.requestsSent.Set(id, struct{}{}) p.Send(PexChannel, cdc.MustMarshalBinaryBare(&pexRequestMessage{})) } @@ -332,7 +332,7 @@ func (r *PEXReactor) RequestAddrs(p Peer) { func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { id := string(src.ID()) if !r.requestsSent.Has(id) { - return errors.New("Unsolicited pexAddrsMessage") + return errors.New("unsolicited pexAddrsMessage") } r.requestsSent.Delete(id) diff --git a/p2p/pex/pex_reactor_test.go b/p2p/pex/pex_reactor_test.go index b16a0d919..f4b7cc265 100644 --- a/p2p/pex/pex_reactor_test.go +++ b/p2p/pex/pex_reactor_test.go @@ -144,7 +144,7 @@ func TestPEXReactorRequestMessageAbuse(t *testing.T) { sw.SetAddrBook(book) peer := mock.NewPeer(nil) - p2p.AddPeerToSwitch(sw, peer) + p2p.AddPeerToSwitchPeerSet(sw, peer) assert.True(t, sw.Peers().Has(peer.ID())) id := string(peer.ID()) @@ -174,7 +174,7 @@ func TestPEXReactorAddrsMessageAbuse(t *testing.T) { sw.SetAddrBook(book) peer := mock.NewPeer(nil) - p2p.AddPeerToSwitch(sw, peer) + p2p.AddPeerToSwitchPeerSet(sw, peer) assert.True(t, sw.Peers().Has(peer.ID())) id := string(peer.ID()) diff --git a/p2p/switch.go b/p2p/switch.go index 66e90bec5..31e0aa6e1 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -324,14 +324,20 @@ func (sw *Switch) StopPeerGracefully(peer Peer) { } func (sw *Switch) stopAndRemovePeer(peer Peer, reason interface{}) { - if sw.peers.Remove(peer) { - sw.metrics.Peers.Add(float64(-1)) - } sw.transport.Cleanup(peer) peer.Stop() + for _, reactor := range sw.reactors { reactor.RemovePeer(peer, reason) } + + // Removing a peer should go last to avoid a situation where a peer + // reconnect to our node and the switch calls InitPeer before + // RemovePeer is finished. + // https://github.com/tendermint/tendermint/issues/3338 + if sw.peers.Remove(peer) { + sw.metrics.Peers.Add(float64(-1)) + } } // reconnectToPeer tries to reconnect to the addr, first repeatedly @@ -739,6 +745,11 @@ func (sw *Switch) addPeer(p Peer) error { return nil } + // Add some data to the peer, which is required by reactors. + for _, reactor := range sw.reactors { + p = reactor.InitPeer(p) + } + // Start the peer's send/recv routines. // Must start it before adding it to the peer set // to prevent Start and Stop from being called concurrently. diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 8485a759f..aa5ca78bf 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -12,6 +12,7 @@ import ( "regexp" "strconv" "sync" + "sync/atomic" "testing" "time" @@ -603,6 +604,71 @@ func TestSwitchAcceptRoutineErrorCases(t *testing.T) { }) } +// mockReactor checks that InitPeer never called before RemovePeer. If that's +// not true, InitCalledBeforeRemoveFinished will return true. +type mockReactor struct { + *BaseReactor + + // atomic + removePeerInProgress uint32 + initCalledBeforeRemoveFinished uint32 +} + +func (r *mockReactor) RemovePeer(peer Peer, reason interface{}) { + atomic.StoreUint32(&r.removePeerInProgress, 1) + defer atomic.StoreUint32(&r.removePeerInProgress, 0) + time.Sleep(100 * time.Millisecond) +} + +func (r *mockReactor) InitPeer(peer Peer) Peer { + if atomic.LoadUint32(&r.removePeerInProgress) == 1 { + atomic.StoreUint32(&r.initCalledBeforeRemoveFinished, 1) + } + + return peer +} + +func (r *mockReactor) InitCalledBeforeRemoveFinished() bool { + return atomic.LoadUint32(&r.initCalledBeforeRemoveFinished) == 1 +} + +// see stopAndRemovePeer +func TestSwitchInitPeerIsNotCalledBeforeRemovePeer(t *testing.T) { + // make reactor + reactor := &mockReactor{} + reactor.BaseReactor = NewBaseReactor("mockReactor", reactor) + + // make switch + sw := MakeSwitch(cfg, 1, "testing", "123.123.123", func(i int, sw *Switch) *Switch { + sw.AddReactor("mock", reactor) + return sw + }) + err := sw.Start() + require.NoError(t, err) + defer sw.Stop() + + // add peer + rp := &remotePeer{PrivKey: ed25519.GenPrivKey(), Config: cfg} + rp.Start() + defer rp.Stop() + _, err = rp.Dial(sw.NetAddress()) + require.NoError(t, err) + // wait till the switch adds rp to the peer set + time.Sleep(50 * time.Millisecond) + + // stop peer asynchronously + go sw.StopPeerForError(sw.Peers().Get(rp.ID()), "test") + + // simulate peer reconnecting to us + _, err = rp.Dial(sw.NetAddress()) + require.NoError(t, err) + // wait till the switch adds rp to the peer set + time.Sleep(50 * time.Millisecond) + + // make sure reactor.RemovePeer is finished before InitPeer is called + assert.False(t, reactor.InitCalledBeforeRemoveFinished()) +} + func BenchmarkSwitchBroadcast(b *testing.B) { s1, s2 := MakeSwitchPair(b, func(i int, sw *Switch) *Switch { // Make bar reactors of bar channels each diff --git a/p2p/test_util.go b/p2p/test_util.go index f8020924c..fa175aeb4 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -27,7 +27,7 @@ func (ni mockNodeInfo) NetAddress() (*NetAddress, error) { return ni.addr, ni func (ni mockNodeInfo) Validate() error { return nil } func (ni mockNodeInfo) CompatibleWith(other NodeInfo) error { return nil } -func AddPeerToSwitch(sw *Switch, peer Peer) { +func AddPeerToSwitchPeerSet(sw *Switch, peer Peer) { sw.peers.Add(peer) } From e7bf25844f1ae8374e84b09089a7591e1eed1840 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Mon, 27 May 2019 14:45:27 -0400 Subject: [PATCH 042/211] update PULL_REQUEST_TEMPLATE and CONTRIBUTING (#3655) * update PULL_REQUEST_TEMPLATE and CONTRIBUTING * Update .github/PULL_REQUEST_TEMPLATE.md Co-Authored-By: Thane Thomson * note ADRs --- .github/PULL_REQUEST_TEMPLATE.md | 9 ++++++++- CONTRIBUTING.md | 8 ++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d4f55392b..e1863c783 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,12 @@ - + + +* [ ] Referenced an issue explaining the need for the change * [ ] Updated all relevant documentation in docs * [ ] Updated all code comments where relevant * [ ] Wrote tests diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2320fb4c..82705f1e8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -4,6 +4,14 @@ Thank you for considering making contributions to Tendermint and related reposit Please follow standard github best practices: fork the repo, branch from the tip of develop, make some commits, and submit a pull request to develop. See the [open issues](https://github.com/tendermint/tendermint/issues) for things we need help with! +Before making a pull request, please open an issue describing the +change you would like to make. If an issue for your change already exists, +please comment on it that you will submit a pull request. Be sure to reference the issue in the opening +comment of your pull request. If your change is substantial, you will be asked +to write a more detailed design document in the form of an +Architectural Decision Record (ie. see [here](./docs/architecture/)) before submitting code +changes. + Please make sure to use `gofmt` before every commit - the easiest way to do this is have your editor run it for you upon saving a file. ## Forking From a6ac611e771503f5dc331e765a60101dfa31195b Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 27 May 2019 15:33:41 -0400 Subject: [PATCH 043/211] tendermint testnet: Allow for better hostname control (#3661) * Allow testnet hostnames to be overridden This allows one to specify the `--hostname` flag multiple times, each time providing an additional custom hostname for a respective peer (validator or non-validator). This overrides any of the `--hostname-prefix` or `--starting-ip-address` flags. The string array approach is taken instead of the string slice approach (see the pflag docs: https://godoc.org/github.com/spf13/pflag#StringArray) because the string slice approach (a comma-separated string) doesn't allow for cleaner multi-line BASH scripts - where this feature is intended to be used. * Reorder conditional for clarity with simpler earlier return * Allow for specifying peer hostname suffix * Quote values in help strings for greater clarity * Fix command switch * Add CHANGELOG_PENDING entry for PR * Allow for unique monikers The current approach to generating monikers for testnet nodes assigns the local hostname of the machine on which the testnet config was generated to all nodes. This results in the same moniker for each and every node. This commit makes use of the supplied `--hostname-prefix` and `--hostname-suffix`, or `--hostname` parameters to generate unique monikers for each node. Alternatively, another parameter (`--random-monikers`) allows one to forcibly override all of the other options with random hexadecimal strings. * Update CHANGELOG_PENDING entry for new command line switch --- CHANGELOG_PENDING.md | 5 ++- cmd/tendermint/commands/testnet.go | 66 +++++++++++++++++++++++------- 2 files changed, 55 insertions(+), 16 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index a25afbb19..af4a51b32 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -35,8 +35,11 @@ - [p2p] [\#3463](https://github.com/tendermint/tendermint/pull/3463) Do not log "Can't add peer's address to addrbook" error for a private peer - [p2p] [\#3552](https://github.com/tendermint/tendermint/pull/3552) Add PeerBehaviour Interface (@brapse) - [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC +- [cli] [\#3661](https://github.com/tendermint/tendermint/pull/3661) Add + `--hostname-suffix`, `--hostname` and `--random-monikers` options to `testnet` + cmd for greater peer address/identity generation flexibility. - [cli] \#3585 Add option to not clear address book with unsafe reset (@climber73) -- [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `-config=` option to `testnet` cmd (@gregdhill) +- [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `--config=` option to `testnet` cmd (@gregdhill) - [cs/replay] \#3460 check appHash for each block - [rpc] \#3362 `/dial_seeds` & `/dial_peers` return errors if addresses are incorrect (except when IP lookup fails) - [node] \#3362 returns an error if `persistent_peers` list is invalid (except when IP lookup fails) diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index b4b33e655..f1dd6f16e 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -27,8 +27,11 @@ var ( populatePersistentPeers bool hostnamePrefix string + hostnameSuffix string startingIPAddress string + hostnames []string p2pPort int + randomMonikers bool ) const ( @@ -50,11 +53,17 @@ func init() { TestnetFilesCmd.Flags().BoolVar(&populatePersistentPeers, "populate-persistent-peers", true, "Update config of each node with the list of persistent peers build using either hostname-prefix or starting-ip-address") TestnetFilesCmd.Flags().StringVar(&hostnamePrefix, "hostname-prefix", "node", - "Hostname prefix (node results in persistent peers list ID0@node0:26656, ID1@node1:26656, ...)") + "Hostname prefix (\"node\" results in persistent peers list ID0@node0:26656, ID1@node1:26656, ...)") + TestnetFilesCmd.Flags().StringVar(&hostnameSuffix, "hostname-suffix", "", + "Hostname suffix (\".xyz.com\" results in persistent peers list ID0@node0.xyz.com:26656, ID1@node1.xyz.com:26656, ...)") TestnetFilesCmd.Flags().StringVar(&startingIPAddress, "starting-ip-address", "", - "Starting IP address (192.168.0.1 results in persistent peers list ID0@192.168.0.1:26656, ID1@192.168.0.2:26656, ...)") + "Starting IP address (\"192.168.0.1\" results in persistent peers list ID0@192.168.0.1:26656, ID1@192.168.0.2:26656, ...)") + TestnetFilesCmd.Flags().StringArrayVar(&hostnames, "hostname", []string{}, + "Manually override all hostnames of validators and non-validators (use --hostname multiple times for multiple hosts)") TestnetFilesCmd.Flags().IntVar(&p2pPort, "p2p-port", 26656, "P2P Port") + TestnetFilesCmd.Flags().BoolVar(&randomMonikers, "random-monikers", false, + "Randomize the moniker for each generated node") } // TestnetFilesCmd allows initialisation of files for a Tendermint testnet. @@ -76,6 +85,13 @@ Example: } func testnetFiles(cmd *cobra.Command, args []string) error { + if len(hostnames) > 0 && len(hostnames) != (nValidators+nNonValidators) { + return fmt.Errorf( + "testnet needs precisely %d hostnames (number of validators plus non-validators) if --hostname parameter is used", + nValidators+nNonValidators, + ) + } + config := cfg.DefaultConfig() // overwrite default config if set and valid @@ -181,6 +197,7 @@ func testnetFiles(cmd *cobra.Command, args []string) error { if populatePersistentPeers { config.P2P.PersistentPeers = persistentPeers } + config.Moniker = moniker(i) cfg.WriteConfigFile(filepath.Join(nodeDir, "config", "config.toml"), config) } @@ -190,21 +207,23 @@ func testnetFiles(cmd *cobra.Command, args []string) error { } func hostnameOrIP(i int) string { - if startingIPAddress != "" { - ip := net.ParseIP(startingIPAddress) - ip = ip.To4() - if ip == nil { - fmt.Printf("%v: non ipv4 address\n", startingIPAddress) - os.Exit(1) - } - - for j := 0; j < i; j++ { - ip[3]++ - } - return ip.String() + if len(hostnames) > 0 && i < len(hostnames) { + return hostnames[i] + } + if startingIPAddress == "" { + return fmt.Sprintf("%s%d%s", hostnamePrefix, i, hostnameSuffix) + } + ip := net.ParseIP(startingIPAddress) + ip = ip.To4() + if ip == nil { + fmt.Printf("%v: non ipv4 address\n", startingIPAddress) + os.Exit(1) } - return fmt.Sprintf("%s%d", hostnamePrefix, i) + for j := 0; j < i; j++ { + ip[3]++ + } + return ip.String() } func persistentPeersString(config *cfg.Config) (string, error) { @@ -220,3 +239,20 @@ func persistentPeersString(config *cfg.Config) (string, error) { } return strings.Join(persistentPeers, ","), nil } + +func moniker(i int) string { + if randomMonikers { + return randomMoniker() + } + if len(hostnames) > 0 && i < len(hostnames) { + return hostnames[i] + } + if startingIPAddress == "" { + return fmt.Sprintf("%s%d%s", hostnamePrefix, i, hostnameSuffix) + } + return randomMoniker() +} + +func randomMoniker() string { + return cmn.HexBytes(cmn.RandBytes(8)).String() +} From b9508ffecba0ded9b3a401f1acaba67e273ff053 Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Mon, 27 May 2019 21:44:56 +0200 Subject: [PATCH 044/211] [p2p] Peer behaviour test tweaks (#3662) * Peer behaviour test tweaks: Address the remaining test issues mentioned in ##3552 notably: + switch to p2p_test package + Use `Error` instead of `Errorf` when not using formatting + Add expected/got errors to `TestMockPeerBehaviourReporterConcurrency` test * Peer behaviour equal behaviours test + slices of PeerBehaviours should be compared as histograms to ensure they have the same set of PeerBehaviours at the same freequncy. * TestEqualPeerBehaviours: + Add tests for the equivalence between sets of PeerBehaviours --- p2p/peer_behaviour_test.go | 129 ++++++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 32 deletions(-) diff --git a/p2p/peer_behaviour_test.go b/p2p/peer_behaviour_test.go index 03ea4969f..1822844d6 100644 --- a/p2p/peer_behaviour_test.go +++ b/p2p/peer_behaviour_test.go @@ -1,58 +1,121 @@ -package p2p +package p2p_test import ( - "net" "sync" "testing" + + "github.com/tendermint/tendermint/p2p" ) // TestMockPeerBehaviour tests the MockPeerBehaviour' ability to store reported // peer behaviour in memory indexed by the peerID func TestMockPeerBehaviourReporter(t *testing.T) { - peer := newMockPeer(net.IP{127, 0, 0, 1}) - pr := NewMockPeerBehaviourReporter() + var peerID p2p.ID = "MockPeer" + pr := p2p.NewMockPeerBehaviourReporter() - behaviours := pr.GetBehaviours(peer.ID()) + behaviours := pr.GetBehaviours(peerID) if len(behaviours) != 0 { - t.Errorf("Expected to have no behaviours reported") + t.Error("Expected to have no behaviours reported") } - pr.Report(peer.ID(), PeerBehaviourBadMessage) - behaviours = pr.GetBehaviours(peer.ID()) + pr.Report(peerID, p2p.PeerBehaviourBadMessage) + behaviours = pr.GetBehaviours(peerID) if len(behaviours) != 1 { - t.Errorf("Expected the peer have one reported behaviour") + t.Error("Expected the peer have one reported behaviour") } - if behaviours[0] != PeerBehaviourBadMessage { - t.Errorf("Expected PeerBehaviourBadMessage to have been reported") + if behaviours[0] != p2p.PeerBehaviourBadMessage { + t.Error("Expected PeerBehaviourBadMessage to have been reported") } } type scriptedBehaviours struct { - PeerID ID - Behaviours []PeerBehaviour + PeerID p2p.ID + Behaviours []p2p.PeerBehaviour } type scriptItem struct { - PeerID ID - Behaviour PeerBehaviour + PeerID p2p.ID + Behaviour p2p.PeerBehaviour } -func equalBehaviours(a []PeerBehaviour, b []PeerBehaviour) bool { - if len(a) != len(b) { +// equalBehaviours returns true if a and b contain the same PeerBehaviours with +// the same freequency and otherwise false. +func equalBehaviours(a []p2p.PeerBehaviour, b []p2p.PeerBehaviour) bool { + aHistogram := map[p2p.PeerBehaviour]int{} + bHistogram := map[p2p.PeerBehaviour]int{} + + for _, behaviour := range a { + aHistogram[behaviour] += 1 + } + + for _, behaviour := range b { + bHistogram[behaviour] += 1 + } + + if len(aHistogram) != len(bHistogram) { return false } - same := make([]PeerBehaviour, len(a)) - for i, aBehaviour := range a { - for _, bBehaviour := range b { - if aBehaviour == bBehaviour { - same[i] = aBehaviour - } + for _, behaviour := range a { + if aHistogram[behaviour] != bHistogram[behaviour] { + return false } } - return len(same) == len(a) + for _, behaviour := range b { + if bHistogram[behaviour] != aHistogram[behaviour] { + return false + } + } + + return true +} + +// TestEqualPeerBehaviours tests that equalBehaviours can tell that two slices +// of peer behaviours can be compared for the behaviours they contain and the +// freequencies that those behaviours occur. +func TestEqualPeerBehaviours(t *testing.T) { + equals := []struct { + left []p2p.PeerBehaviour + right []p2p.PeerBehaviour + }{ + // Empty sets + {[]p2p.PeerBehaviour{}, []p2p.PeerBehaviour{}}, + // Single behaviours + {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote}, []p2p.PeerBehaviour{p2p.PeerBehaviourVote}}, + // Equal Frequencies + {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}, + []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, + // Equal frequencies different orders + {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourBlockPart}, + []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote}}, + } + + for _, test := range equals { + if !equalBehaviours(test.left, test.right) { + t.Errorf("Expected %#v and %#v to be equal", test.left, test.right) + } + } + + unequals := []struct { + left []p2p.PeerBehaviour + right []p2p.PeerBehaviour + }{ + // Comparing empty sets to non empty sets + {[]p2p.PeerBehaviour{}, []p2p.PeerBehaviour{p2p.PeerBehaviourVote}}, + // Different behaviours + {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote}, []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart}}, + // Same behaviour with different frequencies + {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote}, + []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, + } + + for _, test := range unequals { + if equalBehaviours(test.left, test.right) { + t.Errorf("Expected %#v and %#v to be unequal", test.left, test.right) + } + } } // TestPeerBehaviourConcurrency constructs a scenario in which @@ -61,15 +124,15 @@ func equalBehaviours(a []PeerBehaviour, b []PeerBehaviour) bool { // be used within a Reactor Receive method tests to ensure thread safety. func TestMockPeerBehaviourReporterConcurrency(t *testing.T) { behaviourScript := []scriptedBehaviours{ - {"1", []PeerBehaviour{PeerBehaviourVote}}, - {"2", []PeerBehaviour{PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote}}, - {"3", []PeerBehaviour{PeerBehaviourBlockPart, PeerBehaviourVote, PeerBehaviourBlockPart, PeerBehaviourVote}}, - {"4", []PeerBehaviour{PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote, PeerBehaviourVote}}, - {"5", []PeerBehaviour{PeerBehaviourBlockPart, PeerBehaviourVote, PeerBehaviourBlockPart, PeerBehaviourVote}}, + {"1", []p2p.PeerBehaviour{p2p.PeerBehaviourVote}}, + {"2", []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, + {"3", []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote, p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote}}, + {"4", []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, + {"5", []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote, p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote}}, } var receiveWg sync.WaitGroup - pr := NewMockPeerBehaviourReporter() + pr := p2p.NewMockPeerBehaviourReporter() scriptItems := make(chan scriptItem) done := make(chan int) numConsumers := 3 @@ -108,8 +171,10 @@ func TestMockPeerBehaviourReporterConcurrency(t *testing.T) { receiveWg.Wait() for _, items := range behaviourScript { - if !equalBehaviours(pr.GetBehaviours(items.PeerID), items.Behaviours) { - t.Errorf("Expected peer %s to have behaved \n", items.PeerID) + reported := pr.GetBehaviours(items.PeerID) + if !equalBehaviours(reported, items.Behaviours) { + t.Errorf("Expected peer %s to have behaved \nExpected: %#v \nGot %#v \n", + items.PeerID, items.Behaviours, reported) } } } From b522ad005242e633aa72ee672373043bdcedec2b Mon Sep 17 00:00:00 2001 From: Girish Ramnani Date: Wed, 29 May 2019 14:02:34 +0530 Subject: [PATCH 045/211] docs: fix minor typo (#3681) --- docs/app-dev/subscribing-to-events-via-websocket.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/app-dev/subscribing-to-events-via-websocket.md b/docs/app-dev/subscribing-to-events-via-websocket.md index d745769c3..890b061bf 100644 --- a/docs/app-dev/subscribing-to-events-via-websocket.md +++ b/docs/app-dev/subscribing-to-events-via-websocket.md @@ -2,7 +2,7 @@ Tendermint emits different events, to which you can subscribe via [Websocket](https://en.wikipedia.org/wiki/WebSocket). This can be useful -for third-party applications (for analysys) or inspecting state. +for third-party applications (for analysis) or inspecting state. [List of events](https://godoc.org/github.com/tendermint/tendermint/types#pkg-constants) From 9dcee69ac203aa329f04cc6090d6444aee6564c4 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 31 May 2019 07:25:21 +0900 Subject: [PATCH 046/211] v0.31.6 changelog (#3688) * update changelog * Update changelog - less detail about internal stuff like the `mempool` - focus BUG FIX entries more on what was fixed rather than how * minor cleanup. - remove entry for GenesisDocProvider, it's being undone * minor fix --- CHANGELOG.md | 70 ++++++++++++++++++++++++++++++++++++++++++++ CHANGELOG_PENDING.md | 52 +------------------------------- UPGRADING.md | 10 +++++++ types/block.go | 1 + 4 files changed, 82 insertions(+), 51 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d498060b..4328d4b62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,75 @@ # Changelog +## v0.31.6 + +*May 31st, 2019* + +This release contains many fixes and improvements, primarily for p2p functionality. +It also fixes a security issue in the mempool package. + +With this release, Tendermint now supports [boltdb](https://github.com/etcd-io/bbolt), although +in experimental mode. Feel free to try and report to us any findings/issues. +Note also that the build tags for compiling CLevelDB have changed. + +Special thanks to external contributors on this release: +@guagualvcha, @james-ray, @gregdhill, @climber73, @yutianwu, +@carlosflrs, @defunctzombie, @leoluk, @needkane, @CrocdileChan + +### BREAKING CHANGES: + +* Go API + - [libs/common] Removed deprecated `PanicSanity`, `PanicCrisis`, + `PanicConsensus` and `PanicQ` + - [mempool, state] [\#2659](https://github.com/tendermint/tendermint/issues/2659) `Mempool` now an interface that lives in the mempool package. + See issue and PR for more details. + - [p2p] [\#3346](https://github.com/tendermint/tendermint/issues/3346) `Reactor#InitPeer` method is added to `Reactor` interface + - [types] [\#1648](https://github.com/tendermint/tendermint/issues/1648) `Commit#VoteSignBytes` signature was changed + +### FEATURES: +- [node] [\#2659](https://github.com/tendermint/tendermint/issues/2659) Add `node.Mempool()` method, which allows you to access mempool +- [libs/db] [\#3604](https://github.com/tendermint/tendermint/pull/3604) Add experimental support for bolt db (etcd's fork of bolt) (@CrocdileChan) + +### IMPROVEMENTS: +- [cli] [\#3585](https://github.com/tendermint/tendermint/issues/3585) Add `--keep-addr-book` option to `unsafe_reset_all` cmd to not + clear the address book (@climber73) +- [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add + `--config=` option to `testnet` cmd (@gregdhill) +- [cli] [\#3661](https://github.com/tendermint/tendermint/pull/3661) Add + `--hostname-suffix`, `--hostname` and `--random-monikers` options to `testnet` + cmd for greater peer address/identity generation flexibility. +- [crypto] [\#3672](https://github.com/tendermint/tendermint/issues/3672) Return more info in the `AddSignatureFromPubKey` error +- [cs/replay] [\#3460](https://github.com/tendermint/tendermint/issues/3460) Check appHash for each block +- [libs/db] [\#3611](https://github.com/tendermint/tendermint/issues/3611) Conditional compilation + * Use `cleveldb` tag instead of `gcc` to compile Tendermint with CLevelDB or + use `make build_c` / `make install_c` (full instructions can be found at + https://tendermint.com/docs/introduction/install.html#compile-with-cleveldb-support) + * Use `boltdb` tag to compile Tendermint with bolt db +- [node] [\#3362](https://github.com/tendermint/tendermint/issues/3362) Return an error if `persistent_peers` list is invalid (except + when IP lookup fails) +- [p2p] [\#3463](https://github.com/tendermint/tendermint/pull/3463) Do not log "Can't add peer's address to addrbook" error for a private peer (@guagualvcha) +- [p2p] [\#3531](https://github.com/tendermint/tendermint/issues/3531) Terminate session on nonce wrapping (@climber73) +- [pex] [\#3647](https://github.com/tendermint/tendermint/pull/3647) Dial seeds, if any, instead of crawling peers first (@defunctzombie) +- [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC +- [rpc] [\#3362](https://github.com/tendermint/tendermint/issues/3362) `/dial_seeds` & `/dial_peers` return errors if addresses are + incorrect (except when IP lookup fails) + +### BUG FIXES: +- [consensus] [\#3067](https://github.com/tendermint/tendermint/issues/3067) Fix replay from appHeight==0 with validator set changes (@james-ray) +- [consensus] [\#3304](https://github.com/tendermint/tendermint/issues/3304) Create a peer state in consensus reactor before the peer + is started (@guagualvcha) +- [lite] [\#3669](https://github.com/tendermint/tendermint/issues/3669) Add context parameter to RPC Handlers in proxy routes (@yutianwu) +- [mempool] [\#3322](https://github.com/tendermint/tendermint/issues/3322) When a block is committed, only remove committed txs from the mempool +that were valid (ie. `ResponseDeliverTx.Code == 0`) +- [p2p] [\#3338](https://github.com/tendermint/tendermint/issues/3338) Ensure `RemovePeer` is always called before `InitPeer` (upon a peer + reconnecting to our node) +- [p2p] [\#3532](https://github.com/tendermint/tendermint/issues/3532) Limit the number of attempts to connect to a peer in seed mode + to 16 (as a result, the node will stop retrying after a 35 hours time window) +- [p2p] [\#3362](https://github.com/tendermint/tendermint/issues/3362) Allow inbound peers to be persistent, including for seed nodes. +- [pex] [\#3603](https://github.com/tendermint/tendermint/pull/3603) Dial seeds when addrbook needs more addresses (@defunctzombie) + +### OTHERS: +- [networks] fixes ansible integration script (@carlosflrs) + ## v0.31.5 *April 16th, 2019* diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index af4a51b32..9b8fc1cf0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,4 +1,4 @@ -## v0.31.6 +## v0.31.7 ** @@ -9,63 +9,13 @@ * Apps * Go API -- [mempool] \#2659 `Mempool` now an interface - * old `Mempool` implementation renamed to `CListMempool` - * `NewMempool` renamed to `NewCListMempool` - * `Option` renamed to `CListOption` - * unexpose `MempoolReactor.Mempool` - * `MempoolReactor` renamed to `Reactor` - * `NewMempoolReactor` renamed to `NewReactor` - * unexpose `TxID` method - * `TxInfo.PeerID` renamed to `SenderID` -- [state] \#2659 `Mempool` interface moved to mempool package - * `MockMempool` moved to top-level mock package and renamed to `Mempool` -- [libs/common] Removed `PanicSanity`, `PanicCrisis`, `PanicConsensus` and `PanicQ` -- [node] Moved `GenesisDocProvider` and `DefaultGenesisDocProviderFunc` to state package -- [p2p] \#3346 Reactor#InitPeer method is added to Reactor interface * Blockchain Protocol * P2P Protocol ### FEATURES: -- [node] \#2659 Add `node.Mempool()` method, which allows you to access mempool ### IMPROVEMENTS: -- [p2p] [\#3463](https://github.com/tendermint/tendermint/pull/3463) Do not log "Can't add peer's address to addrbook" error for a private peer -- [p2p] [\#3552](https://github.com/tendermint/tendermint/pull/3552) Add PeerBehaviour Interface (@brapse) -- [rpc] [\#3534](https://github.com/tendermint/tendermint/pull/3534) Add support for batched requests/responses in JSON RPC -- [cli] [\#3661](https://github.com/tendermint/tendermint/pull/3661) Add - `--hostname-suffix`, `--hostname` and `--random-monikers` options to `testnet` - cmd for greater peer address/identity generation flexibility. -- [cli] \#3585 Add option to not clear address book with unsafe reset (@climber73) -- [cli] [\#3160](https://github.com/tendermint/tendermint/issues/3160) Add `--config=` option to `testnet` cmd (@gregdhill) -- [cs/replay] \#3460 check appHash for each block -- [rpc] \#3362 `/dial_seeds` & `/dial_peers` return errors if addresses are incorrect (except when IP lookup fails) -- [node] \#3362 returns an error if `persistent_peers` list is invalid (except when IP lookup fails) -- [p2p] \#3531 Terminate session on nonce wrapping (@climber73) -- [libs/db] \#3611 Conditional compilation - * Use `cleveldb` tag instead of `gcc` to compile Tendermint with CLevelDB or - use `make build_c` / `make install_c` (full instructions can be found at - https://tendermint.com/docs/introduction/install.html#compile-with-cleveldb-support) - * Use `boltdb` tag to compile Tendermint with bolt db ### BUG FIXES: -- [p2p] \#3532 limit the number of attempts to connect to a peer in seed mode - to 16 (as a result, the node will stop retrying after a 35 hours time window) -- [consensus] \#2723, \#3451 and \#3317 Fix non-deterministic tests -- [consensus] \#3346 Create a peer state in consensus reactor before the peer - is started (@guagualvcha) -- [p2p] \#3338 Ensure RemovePeer is always called before InitPeer (upon a peer - reconnecting to our node) -- [p2p] \#3362 make persistent prop independent of conn direction - * `Switch#DialPeersAsync` now only takes a list of peers - * `Switch#DialPeerWithAddress` now only takes an address -- [consensus] \#3067 getBeginBlockValidatorInfo loads validators from stateDB instead of state (@james-ray) -- [pex] \#3603 Dial seeds when addrbook needs more addresses (@defunctzombie) -- [mempool] \#3322 Remove only valid (Code==0) txs on Update - * `Mempool#Update` and `BlockExecutor#Commit` now accept - `[]*abci.ResponseDeliverTx` - list of `DeliverTx` responses, which should - match `block.Txs` -- [networks] fixes ansible integration script (@carlosflrs) -- [lite] \#3669 Add context parameter to RPC Handlers in proxy routes (@yutianwu) diff --git a/UPGRADING.md b/UPGRADING.md index eccb954d3..ccd4f2d7e 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,16 @@ This guide provides steps to be followed when you upgrade your applications to a newer version of Tendermint Core. +## v0.31.6 + +There are no breaking changes in this release except Go API of p2p and +mempool packages. Hovewer, if you're using cleveldb, you'll need to change +the compilation tag: + +Use `cleveldb` tag instead of `gcc` to compile Tendermint with CLevelDB or +use `make build_c` / `make install_c` (full instructions can be found at +https://tendermint.com/docs/introduction/install.html#compile-with-cleveldb-support) + ## v0.31.0 This release contains a breaking change to the behaviour of the pubsub system. diff --git a/types/block.go b/types/block.go index d793ce9dd..313eb6b75 100644 --- a/types/block.go +++ b/types/block.go @@ -584,6 +584,7 @@ func (commit *Commit) VoteSignBytes(chainID string, valIdx int) []byte { // memoizeHeightRound memoizes the height and round of the commit using // the first non-nil vote. +// Should be called before any attempt to access `commit.height` or `commit.round`. func (commit *Commit) memoizeHeightRound() { if len(commit.Precommits) == 0 { return From 0dd6b92a642b96cc07e37b49de262712a3c293ae Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Thu, 30 May 2019 18:40:17 -0400 Subject: [PATCH 047/211] Relocate GenesisDocProvider and DefaultGenesisDocProviderFunc (#3693) * Move GenesisDocProvider and DefaultGenesisDocProviderFunc GenesisDocProvider, being a provider of *types.GenesisDoc, makes sense to be part of the types package. DefaultGenesisDocProviderFunc, which relies on *config.Config to produce a types.GenesisDocProvider, makes sense being part of the config package. * Add aliases to avoid breaking node package API * Revert to original structure After discussion, it appears as though the best place for the relocated structures is still in the node package. This means that for the v0.31.6 release and into the future, there will be no changes two these two entities' APIs. --- node/node.go | 69 +++++++++++++++++++++++++++++++++++++++++++-- rpc/test/helpers.go | 6 ++-- state/store.go | 65 ------------------------------------------ 3 files changed, 68 insertions(+), 72 deletions(-) diff --git a/node/node.go b/node/node.go index 2d7b3b3bb..0d05deee1 100644 --- a/node/node.go +++ b/node/node.go @@ -65,6 +65,19 @@ func DefaultDBProvider(ctx *DBContext) (dbm.DB, error) { return dbm.NewDB(ctx.ID, dbType, ctx.Config.DBDir()), nil } +// GenesisDocProvider returns a GenesisDoc. +// It allows the GenesisDoc to be pulled from sources other than the +// filesystem, for instance from a distributed key-value store cluster. +type GenesisDocProvider func() (*types.GenesisDoc, error) + +// DefaultGenesisDocProviderFunc returns a GenesisDocProvider that loads +// the GenesisDoc from the config.GenesisFile() on the filesystem. +func DefaultGenesisDocProviderFunc(config *cfg.Config) GenesisDocProvider { + return func() (*types.GenesisDoc, error) { + return types.GenesisDocFromFile(config.GenesisFile()) + } +} + // NodeProvider takes a config and a logger and returns a ready to go Node. type NodeProvider func(*cfg.Config, log.Logger) (*Node, error) @@ -99,7 +112,7 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { privval.LoadOrGenFilePV(newPrivValKey, newPrivValState), nodeKey, proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), - sm.DefaultGenesisDocProviderFunc(config), + DefaultGenesisDocProviderFunc(config), DefaultDBProvider, DefaultMetricsProvider(config.Instrumentation), logger, @@ -466,7 +479,7 @@ func NewNode(config *cfg.Config, privValidator types.PrivValidator, nodeKey *p2p.NodeKey, clientCreator proxy.ClientCreator, - genesisDocProvider sm.GenesisDocProvider, + genesisDocProvider GenesisDocProvider, dbProvider DBProvider, metricsProvider MetricsProvider, logger log.Logger) (*Node, error) { @@ -476,7 +489,7 @@ func NewNode(config *cfg.Config, return nil, err } - state, genDoc, err := sm.LoadStateFromDBOrGenesisDocProvider(stateDB, genesisDocProvider) + state, genDoc, err := LoadStateFromDBOrGenesisDocProvider(stateDB, genesisDocProvider) if err != nil { return nil, err } @@ -1002,6 +1015,56 @@ func makeNodeInfo( //------------------------------------------------------------------------------ +var ( + genesisDocKey = []byte("genesisDoc") +) + +// LoadStateFromDBOrGenesisDocProvider attempts to load the state from the +// database, or creates one using the given genesisDocProvider and persists the +// result to the database. On success this also returns the genesis doc loaded +// through the given provider. +func LoadStateFromDBOrGenesisDocProvider(stateDB dbm.DB, genesisDocProvider GenesisDocProvider) (sm.State, *types.GenesisDoc, error) { + // Get genesis doc + genDoc, err := loadGenesisDoc(stateDB) + if err != nil { + genDoc, err = genesisDocProvider() + if err != nil { + return sm.State{}, nil, err + } + // save genesis doc to prevent a certain class of user errors (e.g. when it + // was changed, accidentally or not). Also good for audit trail. + saveGenesisDoc(stateDB, genDoc) + } + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + if err != nil { + return sm.State{}, nil, err + } + return state, genDoc, nil +} + +// panics if failed to unmarshal bytes +func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) { + b := db.Get(genesisDocKey) + if len(b) == 0 { + return nil, errors.New("Genesis doc not found") + } + var genDoc *types.GenesisDoc + err := cdc.UnmarshalJSON(b, &genDoc) + if err != nil { + panic(fmt.Sprintf("Failed to load genesis doc due to unmarshaling error: %v (bytes: %X)", err, b)) + } + return genDoc, nil +} + +// panics if failed to marshal the given genesis document +func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) { + b, err := cdc.MarshalJSON(genDoc) + if err != nil { + panic(fmt.Sprintf("Failed to save genesis doc due to marshaling error: %v", err)) + } + db.SetSync(genesisDocKey, b) +} + func createAndStartPrivValidatorSocketClient( listenAddr string, logger log.Logger, diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index f5c923bf4..033015d11 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -8,10 +8,8 @@ import ( "strings" "time" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/state" - abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/log" cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" @@ -167,7 +165,7 @@ func NewTendermint(app abci.Application, opts *Options) *nm.Node { panic(err) } node, err := nm.NewNode(config, pv, nodeKey, papp, - state.DefaultGenesisDocProviderFunc(config), + nm.DefaultGenesisDocProviderFunc(config), nm.DefaultDBProvider, nm.DefaultMetricsProvider(config.Instrumentation), logger) diff --git a/state/store.go b/state/store.go index e1fb5ba60..0301bc7c3 100644 --- a/state/store.go +++ b/state/store.go @@ -1,11 +1,9 @@ package state import ( - "errors" "fmt" abci "github.com/tendermint/tendermint/abci/types" - cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/types" @@ -19,23 +17,6 @@ const ( valSetCheckpointInterval = 100000 ) -var ( - genesisDocKey = []byte("genesisDoc") -) - -// GenesisDocProvider returns a GenesisDoc. -// It allows the GenesisDoc to be pulled from sources other than the -// filesystem, for instance from a distributed key-value store cluster. -type GenesisDocProvider func() (*types.GenesisDoc, error) - -// DefaultGenesisDocProviderFunc returns a GenesisDocProvider that loads -// the GenesisDoc from the config.GenesisFile() on the filesystem. -func DefaultGenesisDocProviderFunc(config *cfg.Config) GenesisDocProvider { - return func() (*types.GenesisDoc, error) { - return types.GenesisDocFromFile(config.GenesisFile()) - } -} - //------------------------------------------------------------------------ func calcValidatorsKey(height int64) []byte { @@ -84,52 +65,6 @@ func LoadStateFromDBOrGenesisDoc(stateDB dbm.DB, genesisDoc *types.GenesisDoc) ( return state, nil } -// LoadStateFromDBOrGenesisDocProvider attempts to load the state from the -// database, or creates one using the given genesisDocProvider and persists the -// result to the database. On success this also returns the genesis doc loaded -// through the given provider. -func LoadStateFromDBOrGenesisDocProvider(stateDB dbm.DB, genesisDocProvider GenesisDocProvider) (State, *types.GenesisDoc, error) { - // Get genesis doc - genDoc, err := loadGenesisDoc(stateDB) - if err != nil { - genDoc, err = genesisDocProvider() - if err != nil { - return State{}, nil, err - } - // save genesis doc to prevent a certain class of user errors (e.g. when it - // was changed, accidentally or not). Also good for audit trail. - saveGenesisDoc(stateDB, genDoc) - } - state, err := LoadStateFromDBOrGenesisDoc(stateDB, genDoc) - if err != nil { - return State{}, nil, err - } - return state, genDoc, nil -} - -// panics if failed to unmarshal bytes -func loadGenesisDoc(db dbm.DB) (*types.GenesisDoc, error) { - bytes := db.Get(genesisDocKey) - if len(bytes) == 0 { - return nil, errors.New("Genesis doc not found") - } - var genDoc *types.GenesisDoc - err := cdc.UnmarshalJSON(bytes, &genDoc) - if err != nil { - panic(fmt.Sprintf("Failed to load genesis doc due to unmarshaling error: %v (bytes: %X)", err, bytes)) - } - return genDoc, nil -} - -// panics if failed to marshal the given genesis document -func saveGenesisDoc(db dbm.DB, genDoc *types.GenesisDoc) { - bytes, err := cdc.MarshalJSON(genDoc) - if err != nil { - panic(fmt.Sprintf("Failed to save genesis doc due to marshaling error: %v", err)) - } - db.SetSync(genesisDocKey, bytes) -} - // LoadState loads the State from the database. func LoadState(db dbm.DB) State { return loadState(db, stateKey) From 21bfd7fba9abae059bfcd7781e7d3bcb786daf3d Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 3 Jun 2019 08:41:27 -0400 Subject: [PATCH 048/211] Add check for version/version.go TMCoreSemVer (#3704) --- scripts/release_management/bump-semver.py | 27 ++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/scripts/release_management/bump-semver.py b/scripts/release_management/bump-semver.py index b13a10342..ce56d8d7c 100755 --- a/scripts/release_management/bump-semver.py +++ b/scripts/release_management/bump-semver.py @@ -8,6 +8,7 @@ import re import argparse +import sys def semver(ver): @@ -17,6 +18,18 @@ def semver(ver): return ver +def get_tendermint_version(): + """Extracts the current Tendermint version from version/version.go""" + pattern = re.compile(r"TMCoreSemVer = \"(?P([0-9.]+)+)\"") + with open("version/version.go", "rt") as version_file: + for line in version_file: + m = pattern.search(line) + if m: + return m.group('version') + + return None + + if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--version", help="Version number to bump, e.g.: v1.0.0", required=True, type=semver) @@ -34,4 +47,16 @@ def semver(ver): else: patch = int(patch) + 1 - print("{0}.{1}".format(majorminorprefix, patch)) + expected_version = "{0}.{1}".format(majorminorprefix, patch) + # if we're doing a release + if expected_version != "v0.0.0": + cur_version = get_tendermint_version() + if not cur_version: + print("Failed to obtain Tendermint version from version/version.go") + sys.exit(1) + expected_version_noprefix = expected_version.lstrip("v") + if expected_version_noprefix != "0.0.0" and expected_version_noprefix != cur_version: + print("Expected version/version.go#TMCoreSemVer to be {0}, but was {1}".format(expected_version_noprefix, cur_version)) + sys.exit(1) + + print(expected_version) From 048ac8d94b945698e3737d92cef80064d15faa07 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 3 Jun 2019 21:46:02 +0900 Subject: [PATCH 049/211] remove invalid transactions from the mempool (#3701) Otherwise, we'll be trying to include them in each consecutive block. The downside is that evil proposers will be able to drop valid transactions (see #3322). Reverts https://github.com/tendermint/tendermint/pull/3625 Fixes #3699 --- CHANGELOG_PENDING.md | 2 ++ mempool/clist_mempool.go | 27 +++++++++++++-------------- mempool/clist_mempool_test.go | 7 +++++-- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 9b8fc1cf0..4e83c2dbb 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -19,3 +19,5 @@ ### IMPROVEMENTS: ### BUG FIXES: +- [mempool] \#3699 Revert the change where we only remove valid transactions + from the mempool once a block is committed. diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index c6ff475aa..0d1f3c5b1 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -538,24 +538,23 @@ func (mem *CListMempool) Update( if deliverTxResponses[i].Code == abci.CodeTypeOK { // Add valid committed tx to the cache (if missing). _ = mem.cache.Push(tx) - - // Remove valid committed tx from the mempool. - if e, ok := mem.txsMap.Load(txKey(tx)); ok { - mem.removeTx(tx, e.(*clist.CElement), false) - } } else { // Allow invalid transactions to be resubmitted. mem.cache.Remove(tx) + } - // Don't remove invalid tx from the mempool. - // Otherwise evil proposer can drop valid txs. - // Example: - // 100 -> 101 -> 102 - // Block, proposed by evil proposer: - // 101 -> 102 - // Mempool (if you remove txs): - // 100 - // https://github.com/tendermint/tendermint/issues/3322. + // Remove committed tx from the mempool. + // + // Note an evil proposer can drop valid txs! + // Mempool before: + // 100 -> 101 -> 102 + // Block, proposed by an evil proposer: + // 101 -> 102 + // Mempool after: + // 100 + // https://github.com/tendermint/tendermint/issues/3322. + if e, ok := mem.txsMap.Load(txKey(tx)); ok { + mem.removeTx(tx, e.(*clist.CElement), false) } } diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index 3eb4990b6..bf2c61dd7 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -200,12 +200,15 @@ func TestMempoolUpdate(t *testing.T) { assert.Zero(t, mempool.Size()) } - // 3. Removes invalid transactions from the cache, but leaves them in the mempool (if present) + // 3. Removes invalid transactions from the cache and the mempool (if present) { err := mempool.CheckTx([]byte{0x03}, nil) require.NoError(t, err) mempool.Update(1, []types.Tx{[]byte{0x03}}, abciResponses(1, 1), nil, nil) - assert.Equal(t, 1, mempool.Size()) + assert.Zero(t, mempool.Size()) + + err = mempool.CheckTx([]byte{0x03}, nil) + assert.NoError(t, err) } } From 65a3dfe235b69fbba58f798a7d6522cd6f1244b1 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Mon, 3 Jun 2019 11:34:21 -0400 Subject: [PATCH 050/211] changelog and version (#3709) --- CHANGELOG.md | 15 +++++++++++++++ CHANGELOG_PENDING.md | 4 +--- version/version.go | 2 +- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4328d4b62..068d99c3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## v0.31.7 + +*June 3, 2019* + +This releases fixes a regression in the mempool introduced in v0.31.6. +The regression caused the invalid committed txs to be proposed in blocks over and +over again. + +### BUG FIXES: +- [mempool] \#3699 Remove all committed txs from the mempool. + This reverts the change from v0.31.6 where we only remove valid txs from the mempool. + Note this means malicious proposals can cause txs to be dropped from the + mempools of other nodes by including them in blocks before they are valid. + See \#3322. + ## v0.31.6 *May 31st, 2019* diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 4e83c2dbb..f211a27f3 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,4 +1,4 @@ -## v0.31.7 +## v0.31.8 ** @@ -19,5 +19,3 @@ ### IMPROVEMENTS: ### BUG FIXES: -- [mempool] \#3699 Revert the change where we only remove valid transactions - from the mempool once a block is committed. diff --git a/version/version.go b/version/version.go index a7559d499..1a15717f7 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.31.5" + TMCoreSemVer = "0.31.7" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.0" From 99c9a35982f2cf5522bc8f3eb95d5d29c8ccefaf Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 4 Jun 2019 10:05:22 +0900 Subject: [PATCH 051/211] docs: update RPC docs for /subscribe & /unsubscribe (#3705) Fixes #3678 Co-Authored-By: Ethan Buchman --- rpc/core/events.go | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/rpc/core/events.go b/rpc/core/events.go index 6bc5ecc7a..4b05e5ce1 100644 --- a/rpc/core/events.go +++ b/rpc/core/events.go @@ -50,7 +50,6 @@ import ( // https://godoc.org/github.com/tendermint/tendermint/libs/pubsub/query. // // ```go -// import "github.com/tendermint/tendermint/libs/pubsub/query" // import "github.com/tendermint/tendermint/types" // // client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") @@ -59,15 +58,17 @@ import ( // // handle error // } // defer client.Stop() -// ctx, cancel := context.WithTimeout(context.Background(), timeout) +// ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second) // defer cancel() -// query := query.MustParse("tm.event = 'Tx' AND tx.height = 3") -// txs := make(chan interface{}) -// err = client.Subscribe(ctx, "test-client", query, txs) +// query := "tm.event = 'Tx' AND tx.height = 3" +// txs, err := client.Subscribe(ctx, "test-client", query) +// if err != nil { +// // handle error +// } // // go func() { // for e := range txs { -// fmt.Println("got ", e.(types.EventDataTx)) +// fmt.Println("got ", e.Data.(types.EventDataTx)) // } // }() // ``` @@ -154,7 +155,11 @@ func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, er // // handle error // } // defer client.Stop() -// err = client.Unsubscribe("test-client", query) +// query := "tm.event = 'Tx' AND tx.height = 3" +// err = client.Unsubscribe(context.Background(), "test-client", query) +// if err != nil { +// // handle error +// } // ``` // // > The above command returns JSON structured like this: @@ -198,7 +203,10 @@ func Unsubscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultUnsubscribe // // handle error // } // defer client.Stop() -// err = client.UnsubscribeAll("test-client") +// err = client.UnsubscribeAll(context.Background(), "test-client") +// if err != nil { +// // handle error +// } // ``` // // > The above command returns JSON structured like this: From 96e132b4b0bd234bd3405c1909cdd40f19d2d259 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 4 Jun 2019 12:12:43 +0900 Subject: [PATCH 052/211] libs/db: remove deprecated `LevelDBBackend` const (#3632) --- CHANGELOG_PENDING.md | 3 +++ UPGRADING.md | 8 ++++++++ libs/db/c_level_db.go | 1 - libs/db/db.go | 4 ---- libs/db/go_level_db.go | 1 - 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f211a27f3..c22cb9daf 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -9,6 +9,9 @@ * Apps * Go API +- [libs/db] Removed deprecated `LevelDBBackend` const + * If you have `db_backend` set to `leveldb` in your config file, please + change it to `goleveldb` or `cleveldb`. * Blockchain Protocol diff --git a/UPGRADING.md b/UPGRADING.md index ccd4f2d7e..5a77e0729 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,6 +3,14 @@ This guide provides steps to be followed when you upgrade your applications to a newer version of Tendermint Core. + +## v0.32.0 + +### Config Changes + +If you have `db_backend` set to `leveldb` in your config file, please change it +to `goleveldb` or `cleveldb`. + ## v0.31.6 There are no breaking changes in this release except Go API of p2p and diff --git a/libs/db/c_level_db.go b/libs/db/c_level_db.go index 0ac040d20..7538166b2 100644 --- a/libs/db/c_level_db.go +++ b/libs/db/c_level_db.go @@ -14,7 +14,6 @@ func init() { dbCreator := func(name string, dir string) (DB, error) { return NewCLevelDB(name, dir) } - registerDBCreator(LevelDBBackend, dbCreator, true) registerDBCreator(CLevelDBBackend, dbCreator, false) } diff --git a/libs/db/db.go b/libs/db/db.go index b68204420..d88df398c 100644 --- a/libs/db/db.go +++ b/libs/db/db.go @@ -9,10 +9,6 @@ type DBBackendType string // These are valid backend types. const ( - // LevelDBBackend is a legacy type. Defaults to goleveldb unless cleveldb - // build tag was used, in which it becomes cleveldb. - // Deprecated: Use concrete types (golevedb, cleveldb, etc.) - LevelDBBackend DBBackendType = "leveldb" // GoLevelDBBackend represents goleveldb (github.com/syndtr/goleveldb - most // popular implementation) // - pure go diff --git a/libs/db/go_level_db.go b/libs/db/go_level_db.go index 79ee5ccb9..8c20ccdde 100644 --- a/libs/db/go_level_db.go +++ b/libs/db/go_level_db.go @@ -15,7 +15,6 @@ func init() { dbCreator := func(name string, dir string) (DB, error) { return NewGoLevelDB(name, dir) } - registerDBCreator(LevelDBBackend, dbCreator, false) registerDBCreator(GoLevelDBBackend, dbCreator, false) } From 2a23eca36845b11f6f1755528750889a3ffcacb7 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 5 Jun 2019 10:36:16 +0900 Subject: [PATCH 053/211] docs: update /block_results RPC docs (#3708) Fixes #3616 --- CHANGELOG_PENDING.md | 2 ++ rpc/core/blocks.go | 34 ++++++++++++++++++++++------------ state/store.go | 6 +++--- 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index c22cb9daf..f37904d1b 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -5,6 +5,8 @@ ### BREAKING CHANGES: * CLI/RPC/Config +- [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` -> + `results.deliver_tx`). See docs for details. * Apps diff --git a/rpc/core/blocks.go b/rpc/core/blocks.go index 40b6811db..16ebe9c56 100644 --- a/rpc/core/blocks.go +++ b/rpc/core/blocks.go @@ -339,7 +339,8 @@ func Commit(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultCommit, erro // If no height is provided, it will fetch results for the latest block. // // Results are for the height of the block containing the txs. -// Thus response.results[5] is the results of executing getBlock(h).Txs[5] +// Thus response.results.deliver_tx[5] is the results of executing +// getBlock(h).Txs[5] // // ```shell // curl 'localhost:26657/block_results?height=10' @@ -360,17 +361,27 @@ func Commit(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultCommit, erro // // ```json // { -// "height": "10", -// "results": [ -// { -// "code": "0", -// "data": "CAFE00F00D" -// }, -// { -// "code": "102", -// "data": "" +// "jsonrpc": "2.0", +// "id": "", +// "result": { +// "height": "39", +// "results": { +// "deliver_tx": [ +// { +// "tags": [ +// { +// "key": "YXBwLmNyZWF0b3I=", +// "value": "Q29zbW9zaGkgTmV0b3dva28=" +// } +// ] +// } +// ], +// "end_block": { +// "validator_updates": null +// }, +// "begin_block": {} +// } // } -// ] // } // ``` func BlockResults(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlockResults, error) { @@ -380,7 +391,6 @@ func BlockResults(ctx *rpctypes.Context, heightPtr *int64) (*ctypes.ResultBlockR return nil, err } - // load the results results, err := sm.LoadABCIResponses(stateDB, height) if err != nil { return nil, err diff --git a/state/store.go b/state/store.go index 0301bc7c3..f0bb9e142 100644 --- a/state/store.go +++ b/state/store.go @@ -115,9 +115,9 @@ func saveState(db dbm.DB, state State, key []byte) { // of the various ABCI calls during block processing. // It is persisted to disk for each height before calling Commit. type ABCIResponses struct { - DeliverTx []*abci.ResponseDeliverTx - EndBlock *abci.ResponseEndBlock - BeginBlock *abci.ResponseBeginBlock + DeliverTx []*abci.ResponseDeliverTx `json:"deliver_tx"` + EndBlock *abci.ResponseEndBlock `json:"end_block"` + BeginBlock *abci.ResponseBeginBlock `json:"begin_block"` } // NewABCIResponses returns a new ABCIResponses From c9ef824ddf623a873ca226b98ddb3539c1de1ebc Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Wed, 5 Jun 2019 11:22:00 +0200 Subject: [PATCH 054/211] p2p: Per channel metrics (#3666) (#3677) * Add `chID` label to sent/receive byte mtrics * add changelog pending entry --- CHANGELOG_PENDING.md | 1 + docs/tendermint-core/metrics.md | 56 ++++++++++++++++----------------- p2p/metrics.go | 4 +-- p2p/peer.go | 18 +++++++++-- 4 files changed, 46 insertions(+), 33 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f37904d1b..6c3ff0f95 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -22,5 +22,6 @@ ### FEATURES: ### IMPROVEMENTS: +- [p2p] \#3666 Add per channel telemtry to improve reactor observability ### BUG FIXES: diff --git a/docs/tendermint-core/metrics.md b/docs/tendermint-core/metrics.md index ad6d4c765..94313ddbb 100644 --- a/docs/tendermint-core/metrics.md +++ b/docs/tendermint-core/metrics.md @@ -14,34 +14,34 @@ Listen address can be changed in the config file (see The following metrics are available: -| **Name** | **Type** | **Since** | **Tags** | **Description** | -|-----------------------------------------|-----------|-----------|----------|-----------------------------------------------------------------| -| consensus\_height | Gauge | 0.21.0 | | Height of the chain | -| consensus\_validators | Gauge | 0.21.0 | | Number of validators | -| consensus\_validators\_power | Gauge | 0.21.0 | | Total voting power of all validators | -| consensus\_missing\_validators | Gauge | 0.21.0 | | Number of validators who did not sign | -| consensus\_missing\_validators\_power | Gauge | 0.21.0 | | Total voting power of the missing validators | -| consensus\_byzantine\_validators | Gauge | 0.21.0 | | Number of validators who tried to double sign | -| consensus\_byzantine\_validators\_power | Gauge | 0.21.0 | | Total voting power of the byzantine validators | -| consensus\_block\_interval\_seconds | Histogram | 0.21.0 | | Time between this and last block (Block.Header.Time) in seconds | -| consensus\_rounds | Gauge | 0.21.0 | | Number of rounds | -| consensus\_num\_txs | Gauge | 0.21.0 | | Number of transactions | -| consensus\_block\_parts | counter | on dev | peer\_id | number of blockparts transmitted by peer | -| consensus\_latest\_block\_height | gauge | on dev | | /status sync\_info number | -| consensus\_fast\_syncing | gauge | on dev | | either 0 (not fast syncing) or 1 (syncing) | -| consensus\_total\_txs | Gauge | 0.21.0 | | Total number of transactions committed | -| consensus\_block\_size\_bytes | Gauge | 0.21.0 | | Block size in bytes | -| p2p\_peers | Gauge | 0.21.0 | | Number of peers node's connected to | -| p2p\_peer\_receive\_bytes\_total | counter | on dev | peer\_id | number of bytes received from a given peer | -| p2p\_peer\_send\_bytes\_total | counter | on dev | peer\_id | number of bytes sent to a given peer | -| p2p\_peer\_pending\_send\_bytes | gauge | on dev | peer\_id | number of pending bytes to be sent to a given peer | -| p2p\_num\_txs | gauge | on dev | peer\_id | number of transactions submitted by each peer\_id | -| p2p\_pending\_send\_bytes | gauge | on dev | peer\_id | amount of data pending to be sent to peer | -| mempool\_size | Gauge | 0.21.0 | | Number of uncommitted transactions | -| mempool\_tx\_size\_bytes | histogram | on dev | | transaction sizes in bytes | -| mempool\_failed\_txs | counter | on dev | | number of failed transactions | -| mempool\_recheck\_times | counter | on dev | | number of transactions rechecked in the mempool | -| state\_block\_processing\_time | histogram | on dev | | time between BeginBlock and EndBlock in ms | +| **Name** | **Type** | **Since** | **Tags** | **Description** | +|-----------------------------------------|-----------|-----------|----------------|-----------------------------------------------------------------| +| consensus\_height | Gauge | 0.21.0 | | Height of the chain | +| consensus\_validators | Gauge | 0.21.0 | | Number of validators | +| consensus\_validators\_power | Gauge | 0.21.0 | | Total voting power of all validators | +| consensus\_missing\_validators | Gauge | 0.21.0 | | Number of validators who did not sign | +| consensus\_missing\_validators\_power | Gauge | 0.21.0 | | Total voting power of the missing validators | +| consensus\_byzantine\_validators | Gauge | 0.21.0 | | Number of validators who tried to double sign | +| consensus\_byzantine\_validators\_power | Gauge | 0.21.0 | | Total voting power of the byzantine validators | +| consensus\_block\_interval\_seconds | Histogram | 0.21.0 | | Time between this and last block (Block.Header.Time) in seconds | +| consensus\_rounds | Gauge | 0.21.0 | | Number of rounds | +| consensus\_num\_txs | Gauge | 0.21.0 | | Number of transactions | +| consensus\_block\_parts | counter | on dev | peer\_id | number of blockparts transmitted by peer | +| consensus\_latest\_block\_height | gauge | on dev | | /status sync\_info number | +| consensus\_fast\_syncing | gauge | on dev | | either 0 (not fast syncing) or 1 (syncing) | +| consensus\_total\_txs | Gauge | 0.21.0 | | Total number of transactions committed | +| consensus\_block\_size\_bytes | Gauge | 0.21.0 | | Block size in bytes | +| p2p\_peers | Gauge | 0.21.0 | | Number of peers node's connected to | +| p2p\_peer\_receive\_bytes\_total | counter | on dev | peer\_id, chID | number of bytes per channel received from a given peer | +| p2p\_peer\_send\_bytes\_total | counter | on dev | peer\_id, chID | number of bytes per channel sent to a given peer | +| p2p\_peer\_pending\_send\_bytes | gauge | on dev | peer\_id | number of pending bytes to be sent to a given peer | +| p2p\_num\_txs | gauge | on dev | peer\_id | number of transactions submitted by each peer\_id | +| p2p\_pending\_send\_bytes | gauge | on dev | peer\_id | amount of data pending to be sent to peer | +| mempool\_size | Gauge | 0.21.0 | | Number of uncommitted transactions | +| mempool\_tx\_size\_bytes | histogram | on dev | | transaction sizes in bytes | +| mempool\_failed\_txs | counter | on dev | | number of failed transactions | +| mempool\_recheck\_times | counter | on dev | | number of transactions rechecked in the mempool | +| state\_block\_processing\_time | histogram | on dev | | time between BeginBlock and EndBlock in ms | ## Useful queries diff --git a/p2p/metrics.go b/p2p/metrics.go index 3a6b9568a..675dd9c7c 100644 --- a/p2p/metrics.go +++ b/p2p/metrics.go @@ -47,13 +47,13 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { Subsystem: MetricsSubsystem, Name: "peer_receive_bytes_total", Help: "Number of bytes received from a given peer.", - }, append(labels, "peer_id")).With(labelsAndValues...), + }, append(labels, "peer_id", "chID")).With(labelsAndValues...), PeerSendBytesTotal: prometheus.NewCounterFrom(stdprometheus.CounterOpts{ Namespace: namespace, Subsystem: MetricsSubsystem, Name: "peer_send_bytes_total", Help: "Number of bytes sent to a given peer.", - }, append(labels, "peer_id")).With(labelsAndValues...), + }, append(labels, "peer_id", "chID")).With(labelsAndValues...), PeerPendingSendBytes: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ Namespace: namespace, Subsystem: MetricsSubsystem, diff --git a/p2p/peer.go b/p2p/peer.go index fab3b42d4..80be0db53 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -248,7 +248,11 @@ func (p *peer) Send(chID byte, msgBytes []byte) bool { } res := p.mconn.Send(chID, msgBytes) if res { - p.metrics.PeerSendBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) + labels := []string{ + "peer_id", string(p.ID()), + "chID", fmt.Sprintf("%#x", chID), + } + p.metrics.PeerSendBytesTotal.With(labels...).Add(float64(len(msgBytes))) } return res } @@ -263,7 +267,11 @@ func (p *peer) TrySend(chID byte, msgBytes []byte) bool { } res := p.mconn.TrySend(chID, msgBytes) if res { - p.metrics.PeerSendBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) + labels := []string{ + "peer_id", string(p.ID()), + "chID", fmt.Sprintf("%#x", chID), + } + p.metrics.PeerSendBytesTotal.With(labels...).Add(float64(len(msgBytes))) } return res } @@ -369,7 +377,11 @@ func createMConnection( // which does onPeerError. panic(fmt.Sprintf("Unknown channel %X", chID)) } - p.metrics.PeerReceiveBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) + labels := []string{ + "peer_id", string(p.ID()), + "chID", fmt.Sprintf("%#x", chID), + } + p.metrics.PeerReceiveBytesTotal.With(labels...).Add(float64(len(msgBytes))) reactor.Receive(chID, p, msgBytes) } From a7e8fbf3a7ccfe4ac9fafef6a94c2b9fc9d22d37 Mon Sep 17 00:00:00 2001 From: Haochuan Guo Date: Wed, 5 Jun 2019 17:23:53 +0800 Subject: [PATCH 055/211] rpc: Use Wrap instead of Errorf error (#3686) So we can have an error code when rpc fails. * Update CHANGELOG_PENDING.md Co-Authored-By: Anton Kaliaev * wrap correct error * format code --- CHANGELOG_PENDING.md | 2 ++ rpc/lib/client/http_client.go | 15 ++++++++------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 6c3ff0f95..187caa0f5 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -24,4 +24,6 @@ ### IMPROVEMENTS: - [p2p] \#3666 Add per channel telemtry to improve reactor observability +* [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. + ### BUG FIXES: diff --git a/rpc/lib/client/http_client.go b/rpc/lib/client/http_client.go index 824820fab..3b545a5dd 100644 --- a/rpc/lib/client/http_client.go +++ b/rpc/lib/client/http_client.go @@ -13,9 +13,10 @@ import ( "sync" "github.com/pkg/errors" + amino "github.com/tendermint/go-amino" - cmn "github.com/tendermint/tendermint/libs/common" + cmn "github.com/tendermint/tendermint/libs/common" types "github.com/tendermint/tendermint/rpc/lib/types" ) @@ -303,10 +304,10 @@ func unmarshalResponseBytes(cdc *amino.Codec, responseBytes []byte, expectedID t response := &types.RPCResponse{} err = json.Unmarshal(responseBytes, response) if err != nil { - return nil, errors.Errorf("error unmarshalling rpc response: %v", err) + return nil, errors.Wrap(err, "error unmarshalling rpc response") } if response.Error != nil { - return nil, errors.Errorf("response error: %v", response.Error) + return nil, errors.Wrap(response.Error, "response error") } // From the JSON-RPC 2.0 spec: // id: It MUST be the same as the value of the id member in the Request Object. @@ -316,7 +317,7 @@ func unmarshalResponseBytes(cdc *amino.Codec, responseBytes []byte, expectedID t // Unmarshal the RawMessage into the result. err = cdc.UnmarshalJSON(response.Result, result) if err != nil { - return nil, errors.Errorf("error unmarshalling rpc response result: %v", err) + return nil, errors.Wrap(err, "error unmarshalling rpc response result") } return result, nil } @@ -328,7 +329,7 @@ func unmarshalResponseBytesArray(cdc *amino.Codec, responseBytes []byte, expecte ) err = json.Unmarshal(responseBytes, &responses) if err != nil { - return nil, errors.Errorf("error unmarshalling rpc response: %v", err) + return nil, errors.Wrap(err, "error unmarshalling rpc response") } // No response error checking here as there may be a mixture of successful // and unsuccessful responses. @@ -341,10 +342,10 @@ func unmarshalResponseBytesArray(cdc *amino.Codec, responseBytes []byte, expecte // From the JSON-RPC 2.0 spec: // id: It MUST be the same as the value of the id member in the Request Object. if err := validateResponseID(&response, expectedID); err != nil { - return nil, errors.Errorf("failed to validate response ID in response %d: %v", i, err) + return nil, errors.Wrapf(err, "failed to validate response ID in response %d", i) } if err := cdc.UnmarshalJSON(responses[i].Result, results[i]); err != nil { - return nil, errors.Errorf("error unmarshalling rpc response result: %v", err) + return nil, errors.Wrap(err, "error unmarshalling rpc response result") } } return results, nil From c1f264822abbcfb1099e55a404712a1a16dfdcd1 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 6 Jun 2019 00:39:28 +0900 Subject: [PATCH 056/211] p2p: remove NewNetAddressStringWithOptionalID (#3711) Fixes #3521 The function NewNetAddressStringWithOptionalID is from a time when peer IDs were optional. They're not anymore. So this should be renamed to NewNetAddressString and should ensure the ID is provided. * update changelog * use NewNetAddress in transport tests * use NewNetAddress in TestTransportMultiplexAcceptMultiple --- CHANGELOG_PENDING.md | 1 + node/node.go | 2 +- p2p/netaddress.go | 41 ++++++++------------- p2p/netaddress_test.go | 57 ++++++++++------------------- p2p/pex/pex_reactor.go | 2 +- p2p/transport_test.go | 83 ++++++++++++------------------------------ 6 files changed, 62 insertions(+), 124 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 187caa0f5..8a8ad2795 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -14,6 +14,7 @@ - [libs/db] Removed deprecated `LevelDBBackend` const * If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. +- [p2p] \#3521 Remove NewNetAddressStringWithOptionalID * Blockchain Protocol diff --git a/node/node.go b/node/node.go index 0d05deee1..46185847e 100644 --- a/node/node.go +++ b/node/node.go @@ -674,7 +674,7 @@ func (n *Node) OnStart() error { } // Start the transport. - addr, err := p2p.NewNetAddressStringWithOptionalID(n.config.P2P.ListenAddress) + addr, err := p2p.NewNetAddressString(p2p.IDAddressString(n.nodeKey.ID(), n.config.P2P.ListenAddress)) if err != nil { return err } diff --git a/p2p/netaddress.go b/p2p/netaddress.go index 73130ee5e..d11504525 100644 --- a/p2p/netaddress.go +++ b/p2p/netaddress.go @@ -65,36 +65,27 @@ func NewNetAddress(id ID, addr net.Addr) *NetAddress { // Also resolves the host if host is not an IP. // Errors are of type ErrNetAddressXxx where Xxx is in (NoID, Invalid, Lookup) func NewNetAddressString(addr string) (*NetAddress, error) { - spl := strings.Split(addr, "@") - if len(spl) < 2 { - return nil, ErrNetAddressNoID{addr} - } - return NewNetAddressStringWithOptionalID(addr) -} - -// NewNetAddressStringWithOptionalID returns a new NetAddress using the -// provided address in the form of "ID@IP:Port", where the ID is optional. -// Also resolves the host if host is not an IP. -func NewNetAddressStringWithOptionalID(addr string) (*NetAddress, error) { addrWithoutProtocol := removeProtocolIfDefined(addr) - - var id ID spl := strings.Split(addrWithoutProtocol, "@") - if len(spl) == 2 { - idStr := spl[0] - idBytes, err := hex.DecodeString(idStr) - if err != nil { - return nil, ErrNetAddressInvalid{addrWithoutProtocol, err} - } - if len(idBytes) != IDByteLength { - return nil, ErrNetAddressInvalid{ - addrWithoutProtocol, - fmt.Errorf("invalid hex length - got %d, expected %d", len(idBytes), IDByteLength)} - } + if len(spl) != 2 { + return nil, ErrNetAddressNoID{addr} + } - id, addrWithoutProtocol = ID(idStr), spl[1] + // get ID + idStr := spl[0] + idBytes, err := hex.DecodeString(idStr) + if err != nil { + return nil, ErrNetAddressInvalid{addrWithoutProtocol, err} } + if len(idBytes) != IDByteLength { + return nil, ErrNetAddressInvalid{ + addrWithoutProtocol, + fmt.Errorf("invalid hex length - got %d, expected %d", len(idBytes), IDByteLength)} + } + var id ID + id, addrWithoutProtocol = ID(idStr), spl[1] + // get host and port host, portStr, err := net.SplitHostPort(addrWithoutProtocol) if err != nil { return nil, ErrNetAddressInvalid{addrWithoutProtocol, err} diff --git a/p2p/netaddress_test.go b/p2p/netaddress_test.go index e7b184a76..7afcab131 100644 --- a/p2p/netaddress_test.go +++ b/p2p/netaddress_test.go @@ -20,17 +20,23 @@ func TestNewNetAddress(t *testing.T) { }, "Calling NewNetAddress with UDPAddr should not panic in testing") } -func TestNewNetAddressStringWithOptionalID(t *testing.T) { +func TestNewNetAddressString(t *testing.T) { testCases := []struct { name string addr string expected string correct bool }{ - {"no node id, no protocol", "127.0.0.1:8080", "127.0.0.1:8080", true}, - {"no node id, tcp input", "tcp://127.0.0.1:8080", "127.0.0.1:8080", true}, - {"no node id, udp input", "udp://127.0.0.1:8080", "127.0.0.1:8080", true}, - {"malformed udp input", "udp//127.0.0.1:8080", "", false}, + {"no node id and no protocol", "127.0.0.1:8080", "", false}, + {"no node id w/ tcp input", "tcp://127.0.0.1:8080", "", false}, + {"no node id w/ udp input", "udp://127.0.0.1:8080", "", false}, + + {"no protocol", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, + {"tcp input", "tcp://deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, + {"udp input", "udp://deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, + {"malformed tcp input", "tcp//deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false}, + {"malformed udp input", "udp//deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false}, + // {"127.0.0:8080", false}, {"invalid host", "notahost", "", false}, {"invalid port", "127.0.0.1:notapath", "", false}, @@ -41,14 +47,13 @@ func TestNewNetAddressStringWithOptionalID(t *testing.T) { {"too short nodeId", "deadbeef@127.0.0.1:8080", "", false}, {"too short, not hex nodeId", "this-isnot-hex@127.0.0.1:8080", "", false}, {"not hex nodeId", "xxxxbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false}, - {"correct nodeId", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, {"too short nodeId w/tcp", "tcp://deadbeef@127.0.0.1:8080", "", false}, {"too short notHex nodeId w/tcp", "tcp://this-isnot-hex@127.0.0.1:8080", "", false}, {"notHex nodeId w/tcp", "tcp://xxxxbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "", false}, {"correct nodeId w/tcp", "tcp://deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, - {"no node id when expected", "tcp://@127.0.0.1:8080", "", false}, + {"no node id", "tcp://@127.0.0.1:8080", "", false}, {"no node id or IP", "tcp://@", "", false}, {"tcp no host, w/ port", "tcp://:26656", "", false}, {"empty", "", "", false}, @@ -59,7 +64,7 @@ func TestNewNetAddressStringWithOptionalID(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - addr, err := NewNetAddressStringWithOptionalID(tc.addr) + addr, err := NewNetAddressString(tc.addr) if tc.correct { if assert.Nil(t, err, tc.addr) { assert.Equal(t, tc.expected, addr.String()) @@ -71,28 +76,6 @@ func TestNewNetAddressStringWithOptionalID(t *testing.T) { } } -func TestNewNetAddressString(t *testing.T) { - testCases := []struct { - addr string - expected string - correct bool - }{ - {"127.0.0.1:8080", "127.0.0.1:8080", false}, - {"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true}, - } - - for _, tc := range testCases { - addr, err := NewNetAddressString(tc.addr) - if tc.correct { - if assert.Nil(t, err, tc.addr) { - assert.Equal(t, tc.expected, addr.String()) - } - } else { - assert.NotNil(t, err, tc.addr) - } - } -} - func TestNewNetAddressStrings(t *testing.T) { addrs, errs := NewNetAddressStrings([]string{ "127.0.0.1:8080", @@ -115,12 +98,12 @@ func TestNetAddressProperties(t *testing.T) { local bool routable bool }{ - {"127.0.0.1:8080", true, true, false}, - {"ya.ru:80", true, false, true}, + {"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", true, true, false}, + {"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@ya.ru:80", true, false, true}, } for _, tc := range testCases { - addr, err := NewNetAddressStringWithOptionalID(tc.addr) + addr, err := NewNetAddressString(tc.addr) require.Nil(t, err) assert.Equal(t, tc.valid, addr.Valid()) @@ -136,15 +119,15 @@ func TestNetAddressReachabilityTo(t *testing.T) { other string reachability int }{ - {"127.0.0.1:8080", "127.0.0.1:8081", 0}, - {"ya.ru:80", "127.0.0.1:8080", 1}, + {"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8081", 0}, + {"deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@ya.ru:80", "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", 1}, } for _, tc := range testCases { - addr, err := NewNetAddressStringWithOptionalID(tc.addr) + addr, err := NewNetAddressString(tc.addr) require.Nil(t, err) - other, err := NewNetAddressStringWithOptionalID(tc.other) + other, err := NewNetAddressString(tc.other) require.Nil(t, err) assert.Equal(t, tc.reachability, addr.ReachabilityTo(other)) diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index eabbc4d61..e77fa8eaa 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -345,7 +345,7 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { if netAddr == nil { return errors.New("nil address in pexAddrsMessage") } - // TODO: extract validating logic from NewNetAddressStringWithOptionalID + // TODO: extract validating logic from NewNetAddressString // and put it in netAddr#Valid (#2722) na, err := p2p.NewNetAddressString(netAddr.String()) if err != nil { diff --git a/p2p/transport_test.go b/p2p/transport_test.go index 35fd9c66b..7580f0259 100644 --- a/p2p/transport_test.go +++ b/p2p/transport_test.go @@ -8,8 +8,6 @@ import ( "testing" "time" - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/p2p/conn" ) @@ -39,6 +37,7 @@ func TestTransportMultiplexConnFilter(t *testing.T) { PrivKey: ed25519.GenPrivKey(), }, ) + id := mt.nodeKey.ID() MultiplexTransportConnFilters( func(_ ConnSet, _ net.Conn, _ []net.IP) error { return nil }, @@ -48,7 +47,7 @@ func TestTransportMultiplexConnFilter(t *testing.T) { }, )(mt) - addr, err := NewNetAddressStringWithOptionalID("127.0.0.1:0") + addr, err := NewNetAddressString(IDAddressString(id, "127.0.0.1:0")) if err != nil { t.Fatal(err) } @@ -60,13 +59,9 @@ func TestTransportMultiplexConnFilter(t *testing.T) { errc := make(chan error) go func() { - addr, err := NewNetAddressStringWithOptionalID(mt.listener.Addr().String()) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(id, mt.listener.Addr()) - _, err = addr.Dial() + _, err := addr.Dial() if err != nil { errc <- err return @@ -96,6 +91,7 @@ func TestTransportMultiplexConnFilterTimeout(t *testing.T) { PrivKey: ed25519.GenPrivKey(), }, ) + id := mt.nodeKey.ID() MultiplexTransportFilterTimeout(5 * time.Millisecond)(mt) MultiplexTransportConnFilters( @@ -105,7 +101,7 @@ func TestTransportMultiplexConnFilterTimeout(t *testing.T) { }, )(mt) - addr, err := NewNetAddressStringWithOptionalID("127.0.0.1:0") + addr, err := NewNetAddressString(IDAddressString(id, "127.0.0.1:0")) if err != nil { t.Fatal(err) } @@ -117,13 +113,9 @@ func TestTransportMultiplexConnFilterTimeout(t *testing.T) { errc := make(chan error) go func() { - addr, err := NewNetAddressStringWithOptionalID(mt.listener.Addr().String()) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(id, mt.listener.Addr()) - _, err = addr.Dial() + _, err := addr.Dial() if err != nil { errc <- err return @@ -144,9 +136,7 @@ func TestTransportMultiplexConnFilterTimeout(t *testing.T) { func TestTransportMultiplexAcceptMultiple(t *testing.T) { mt := testSetupMultiplexTransport(t) - id, addr := mt.nodeKey.ID(), mt.listener.Addr().String() - laddr, err := NewNetAddressStringWithOptionalID(IDAddressString(id, addr)) - require.NoError(t, err) + laddr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) var ( seed = rand.New(rand.NewSource(time.Now().UnixNano())) @@ -232,11 +222,7 @@ func TestTransportMultiplexAcceptNonBlocking(t *testing.T) { // Simulate slow Peer. go func() { - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(mt.nodeKey.ID(), mt.listener.Addr().String())) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) c, err := addr.Dial() if err != nil { @@ -283,13 +269,9 @@ func TestTransportMultiplexAcceptNonBlocking(t *testing.T) { }, ) ) - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(mt.nodeKey.ID(), mt.listener.Addr().String())) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err = dialer.Dial(*addr, peerConfig{}) + _, err := dialer.Dial(*addr, peerConfig{}) if err != nil { errc <- err return @@ -329,13 +311,9 @@ func TestTransportMultiplexValidateNodeInfo(t *testing.T) { ) ) - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(mt.nodeKey.ID(), mt.listener.Addr().String())) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err = dialer.Dial(*addr, peerConfig{}) + _, err := dialer.Dial(*addr, peerConfig{}) if err != nil { errc <- err return @@ -372,13 +350,9 @@ func TestTransportMultiplexRejectMissmatchID(t *testing.T) { PrivKey: ed25519.GenPrivKey(), }, ) - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(mt.nodeKey.ID(), mt.listener.Addr().String())) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err = dialer.Dial(*addr, peerConfig{}) + _, err := dialer.Dial(*addr, peerConfig{}) if err != nil { errc <- err return @@ -415,12 +389,9 @@ func TestTransportMultiplexDialRejectWrongID(t *testing.T) { ) wrongID := PubKeyToID(ed25519.GenPrivKey().PubKey()) - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(wrongID, mt.listener.Addr().String())) - if err != nil { - t.Fatalf("invalid address with ID: %v", err) - } + addr := NewNetAddress(wrongID, mt.listener.Addr()) - _, err = dialer.Dial(*addr, peerConfig{}) + _, err := dialer.Dial(*addr, peerConfig{}) if err != nil { t.Logf("connection failed: %v", err) if err, ok := err.(ErrRejected); ok { @@ -448,13 +419,9 @@ func TestTransportMultiplexRejectIncompatible(t *testing.T) { }, ) ) - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(mt.nodeKey.ID(), mt.listener.Addr().String())) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err = dialer.Dial(*addr, peerConfig{}) + _, err := dialer.Dial(*addr, peerConfig{}) if err != nil { errc <- err return @@ -479,13 +446,9 @@ func TestTransportMultiplexRejectSelf(t *testing.T) { errc := make(chan error) go func() { - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(mt.nodeKey.ID(), mt.listener.Addr().String())) - if err != nil { - errc <- err - return - } + addr := NewNetAddress(mt.nodeKey.ID(), mt.listener.Addr()) - _, err = mt.Dial(*addr, peerConfig{}) + _, err := mt.Dial(*addr, peerConfig{}) if err != nil { errc <- err return @@ -609,7 +572,7 @@ func testSetupMultiplexTransport(t *testing.T) *MultiplexTransport { ) ) - addr, err := NewNetAddressStringWithOptionalID(IDAddressString(id, "127.0.0.1:0")) + addr, err := NewNetAddressString(IDAddressString(id, "127.0.0.1:0")) if err != nil { t.Fatal(err) } From 6a7d4182b457a274c75ec411df4de2adc90d4df2 Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Thu, 6 Jun 2019 22:33:39 +0200 Subject: [PATCH 057/211] p2p: Peerbehaviour follow up (#3653) (#3663) * Move peer behaviour into it's own package * refactor wip * Adjust API and fix tests * remove unused test struct * Better error message * Restructure: + Now behaviour is it's own package, we don't need to include PeerBehaviour in every type. + Split up behaviours and reporters into seperate files * doc string fixes * Fix minor typos * Update behaviour/reporter.go Co-Authored-By: Anton Kaliaev * Update behaviour/reporter.go Co-Authored-By: Anton Kaliaev --- behaviour/peer_behaviour.go | 49 ++++++++++ behaviour/reporter.go | 84 ++++++++++++++++ behaviour/reporter_test.go | 186 ++++++++++++++++++++++++++++++++++++ p2p/peer_behaviour.go | 92 ------------------ p2p/peer_behaviour_test.go | 180 ---------------------------------- 5 files changed, 319 insertions(+), 272 deletions(-) create mode 100644 behaviour/peer_behaviour.go create mode 100644 behaviour/reporter.go create mode 100644 behaviour/reporter_test.go delete mode 100644 p2p/peer_behaviour.go delete mode 100644 p2p/peer_behaviour_test.go diff --git a/behaviour/peer_behaviour.go b/behaviour/peer_behaviour.go new file mode 100644 index 000000000..36630f46b --- /dev/null +++ b/behaviour/peer_behaviour.go @@ -0,0 +1,49 @@ +package behaviour + +import ( + "github.com/tendermint/tendermint/p2p" +) + +// PeerBehaviour is a struct describing a behaviour a peer performed. +// `peerID` identifies the peer and reason characterizes the specific +// behaviour performed by the peer. +type PeerBehaviour struct { + peerID p2p.ID + reason interface{} +} + +type badMessage struct { + explanation string +} + +// BadMessage returns a badMessage PeerBehaviour. +func BadMessage(peerID p2p.ID, explanation string) PeerBehaviour { + return PeerBehaviour{peerID: peerID, reason: badMessage{explanation}} +} + +type messageOutOfOrder struct { + explanation string +} + +// MessageOutOfOrder returns a messagOutOfOrder PeerBehaviour. +func MessageOutOfOrder(peerID p2p.ID, explanation string) PeerBehaviour { + return PeerBehaviour{peerID: peerID, reason: badMessage{explanation}} +} + +type consensusVote struct { + explanation string +} + +// ConsensusVote returns a consensusVote PeerBehaviour. +func ConsensusVote(peerID p2p.ID, explanation string) PeerBehaviour { + return PeerBehaviour{peerID: peerID, reason: consensusVote{explanation}} +} + +type blockPart struct { + explanation string +} + +// BlockPart returns blockPart PeerBehaviour. +func BlockPart(peerID p2p.ID, explanation string) PeerBehaviour { + return PeerBehaviour{peerID: peerID, reason: blockPart{explanation}} +} diff --git a/behaviour/reporter.go b/behaviour/reporter.go new file mode 100644 index 000000000..f8a0693bf --- /dev/null +++ b/behaviour/reporter.go @@ -0,0 +1,84 @@ +package behaviour + +import ( + "errors" + "sync" + + "github.com/tendermint/tendermint/p2p" +) + +// Reporter provides an interface for reactors to report the behaviour +// of peers synchronously to other components. +type Reporter interface { + Report(behaviour PeerBehaviour) error +} + +// SwitchReporter reports peer behaviour to an internal Switch. +type SwitchReporter struct { + sw *p2p.Switch +} + +// NewSwitchReporter return a new SwitchReporter instance which wraps the Switch. +func NewSwitcReporter(sw *p2p.Switch) *SwitchReporter { + return &SwitchReporter{ + sw: sw, + } +} + +// Report reports the behaviour of a peer to the Switch. +func (spbr *SwitchReporter) Report(behaviour PeerBehaviour) error { + peer := spbr.sw.Peers().Get(behaviour.peerID) + if peer == nil { + return errors.New("peer not found") + } + + switch reason := behaviour.reason.(type) { + case consensusVote, blockPart: + spbr.sw.MarkPeerAsGood(peer) + case badMessage: + spbr.sw.StopPeerForError(peer, reason.explanation) + case messageOutOfOrder: + spbr.sw.StopPeerForError(peer, reason.explanation) + default: + return errors.New("unknown reason reported") + } + + return nil +} + +// MockReporter is a concrete implementation of the Reporter +// interface used in reactor tests to ensure reactors report the correct +// behaviour in manufactured scenarios. +type MockReporter struct { + mtx sync.RWMutex + pb map[p2p.ID][]PeerBehaviour +} + +// NewMockReporter returns a Reporter which records all reported +// behaviours in memory. +func NewMockReporter() *MockReporter { + return &MockReporter{ + pb: map[p2p.ID][]PeerBehaviour{}, + } +} + +// Report stores the PeerBehaviour produced by the peer identified by peerID. +func (mpbr *MockReporter) Report(behaviour PeerBehaviour) { + mpbr.mtx.Lock() + defer mpbr.mtx.Unlock() + mpbr.pb[behaviour.peerID] = append(mpbr.pb[behaviour.peerID], behaviour) +} + +// GetBehaviours returns all behaviours reported on the peer identified by peerID. +func (mpbr *MockReporter) GetBehaviours(peerID p2p.ID) []PeerBehaviour { + mpbr.mtx.RLock() + defer mpbr.mtx.RUnlock() + if items, ok := mpbr.pb[peerID]; ok { + result := make([]PeerBehaviour, len(items)) + copy(result, items) + + return result + } else { + return []PeerBehaviour{} + } +} diff --git a/behaviour/reporter_test.go b/behaviour/reporter_test.go new file mode 100644 index 000000000..eae94e7bd --- /dev/null +++ b/behaviour/reporter_test.go @@ -0,0 +1,186 @@ +package behaviour_test + +import ( + "sync" + "testing" + + bh "github.com/tendermint/tendermint/behaviour" + "github.com/tendermint/tendermint/p2p" +) + +// TestMockReporter tests the MockReporter's ability to store reported +// peer behaviour in memory indexed by the peerID. +func TestMockReporter(t *testing.T) { + var peerID p2p.ID = "MockPeer" + pr := bh.NewMockReporter() + + behaviours := pr.GetBehaviours(peerID) + if len(behaviours) != 0 { + t.Error("Expected to have no behaviours reported") + } + + badMessage := bh.BadMessage(peerID, "bad message") + pr.Report(badMessage) + behaviours = pr.GetBehaviours(peerID) + if len(behaviours) != 1 { + t.Error("Expected the peer have one reported behaviour") + } + + if behaviours[0] != badMessage { + t.Error("Expected Bad Message to have been reported") + } +} + +type scriptItem struct { + peerID p2p.ID + behaviour bh.PeerBehaviour +} + +// equalBehaviours returns true if a and b contain the same PeerBehaviours with +// the same freequencies and otherwise false. +func equalBehaviours(a []bh.PeerBehaviour, b []bh.PeerBehaviour) bool { + aHistogram := map[bh.PeerBehaviour]int{} + bHistogram := map[bh.PeerBehaviour]int{} + + for _, behaviour := range a { + aHistogram[behaviour] += 1 + } + + for _, behaviour := range b { + bHistogram[behaviour] += 1 + } + + if len(aHistogram) != len(bHistogram) { + return false + } + + for _, behaviour := range a { + if aHistogram[behaviour] != bHistogram[behaviour] { + return false + } + } + + for _, behaviour := range b { + if bHistogram[behaviour] != aHistogram[behaviour] { + return false + } + } + + return true +} + +// TestEqualPeerBehaviours tests that equalBehaviours can tell that two slices +// of peer behaviours can be compared for the behaviours they contain and the +// freequencies that those behaviours occur. +func TestEqualPeerBehaviours(t *testing.T) { + var ( + peerID p2p.ID = "MockPeer" + consensusVote = bh.ConsensusVote(peerID, "voted") + blockPart = bh.BlockPart(peerID, "blocked") + equals = []struct { + left []bh.PeerBehaviour + right []bh.PeerBehaviour + }{ + // Empty sets + {[]bh.PeerBehaviour{}, []bh.PeerBehaviour{}}, + // Single behaviours + {[]bh.PeerBehaviour{consensusVote}, []bh.PeerBehaviour{consensusVote}}, + // Equal Frequencies + {[]bh.PeerBehaviour{consensusVote, consensusVote}, + []bh.PeerBehaviour{consensusVote, consensusVote}}, + // Equal frequencies different orders + {[]bh.PeerBehaviour{consensusVote, blockPart}, + []bh.PeerBehaviour{blockPart, consensusVote}}, + } + unequals = []struct { + left []bh.PeerBehaviour + right []bh.PeerBehaviour + }{ + // Comparing empty sets to non empty sets + {[]bh.PeerBehaviour{}, []bh.PeerBehaviour{consensusVote}}, + // Different behaviours + {[]bh.PeerBehaviour{consensusVote}, []bh.PeerBehaviour{blockPart}}, + // Same behaviour with different frequencies + {[]bh.PeerBehaviour{consensusVote}, + []bh.PeerBehaviour{consensusVote, consensusVote}}, + } + ) + + for _, test := range equals { + if !equalBehaviours(test.left, test.right) { + t.Errorf("Expected %#v and %#v to be equal", test.left, test.right) + } + } + + for _, test := range unequals { + if equalBehaviours(test.left, test.right) { + t.Errorf("Expected %#v and %#v to be unequal", test.left, test.right) + } + } +} + +// TestPeerBehaviourConcurrency constructs a scenario in which +// multiple goroutines are using the same MockReporter instance. +// This test reproduces the conditions in which MockReporter will +// be used within a Reactor `Receive` method tests to ensure thread safety. +func TestMockPeerBehaviourReporterConcurrency(t *testing.T) { + var ( + behaviourScript = []struct { + peerID p2p.ID + behaviours []bh.PeerBehaviour + }{ + {"1", []bh.PeerBehaviour{bh.ConsensusVote("1", "")}}, + {"2", []bh.PeerBehaviour{bh.ConsensusVote("2", ""), bh.ConsensusVote("2", ""), bh.ConsensusVote("2", "")}}, + {"3", []bh.PeerBehaviour{bh.BlockPart("3", ""), bh.ConsensusVote("3", ""), bh.BlockPart("3", ""), bh.ConsensusVote("3", "")}}, + {"4", []bh.PeerBehaviour{bh.ConsensusVote("4", ""), bh.ConsensusVote("4", ""), bh.ConsensusVote("4", ""), bh.ConsensusVote("4", "")}}, + {"5", []bh.PeerBehaviour{bh.BlockPart("5", ""), bh.ConsensusVote("5", ""), bh.BlockPart("5", ""), bh.ConsensusVote("5", "")}}, + } + ) + + var receiveWg sync.WaitGroup + pr := bh.NewMockReporter() + scriptItems := make(chan scriptItem) + done := make(chan int) + numConsumers := 3 + for i := 0; i < numConsumers; i++ { + receiveWg.Add(1) + go func() { + defer receiveWg.Done() + for { + select { + case pb := <-scriptItems: + pr.Report(pb.behaviour) + case <-done: + return + } + } + }() + } + + var sendingWg sync.WaitGroup + sendingWg.Add(1) + go func() { + defer sendingWg.Done() + for _, item := range behaviourScript { + for _, reason := range item.behaviours { + scriptItems <- scriptItem{item.peerID, reason} + } + } + }() + + sendingWg.Wait() + + for i := 0; i < numConsumers; i++ { + done <- 1 + } + + receiveWg.Wait() + + for _, items := range behaviourScript { + reported := pr.GetBehaviours(items.peerID) + if !equalBehaviours(reported, items.behaviours) { + t.Errorf("Expected peer %s to have behaved \nExpected: %#v \nGot %#v \n", + items.peerID, items.behaviours, reported) + } + } +} diff --git a/p2p/peer_behaviour.go b/p2p/peer_behaviour.go deleted file mode 100644 index 8d27805c3..000000000 --- a/p2p/peer_behaviour.go +++ /dev/null @@ -1,92 +0,0 @@ -package p2p - -import ( - "errors" - "sync" -) - -// PeerBehaviour are types of reportable behaviours about peers. -type PeerBehaviour int - -const ( - PeerBehaviourBadMessage = iota - PeerBehaviourMessageOutOfOrder - PeerBehaviourVote - PeerBehaviourBlockPart -) - -// PeerBehaviourReporter provides an interface for reactors to report the behaviour -// of peers synchronously to other components. -type PeerBehaviourReporter interface { - Report(peerID ID, behaviour PeerBehaviour) error -} - -// SwitchPeerBehaviouReporter reports peer behaviour to an internal Switch -type SwitchPeerBehaviourReporter struct { - sw *Switch -} - -// Return a new SwitchPeerBehaviourReporter instance which wraps the Switch. -func NewSwitchPeerBehaviourReporter(sw *Switch) *SwitchPeerBehaviourReporter { - return &SwitchPeerBehaviourReporter{ - sw: sw, - } -} - -// Report reports the behaviour of a peer to the Switch -func (spbr *SwitchPeerBehaviourReporter) Report(peerID ID, behaviour PeerBehaviour) error { - peer := spbr.sw.Peers().Get(peerID) - if peer == nil { - return errors.New("Peer not found") - } - - switch behaviour { - case PeerBehaviourVote, PeerBehaviourBlockPart: - spbr.sw.MarkPeerAsGood(peer) - case PeerBehaviourBadMessage: - spbr.sw.StopPeerForError(peer, "Bad message") - case PeerBehaviourMessageOutOfOrder: - spbr.sw.StopPeerForError(peer, "Message out of order") - default: - return errors.New("Unknown behaviour") - } - - return nil -} - -// MockPeerBehaviourReporter serves a mock concrete implementation of the -// PeerBehaviourReporter interface used in reactor tests to ensure reactors -// report the correct behaviour in manufactured scenarios. -type MockPeerBehaviourReporter struct { - mtx sync.RWMutex - pb map[ID][]PeerBehaviour -} - -// NewMockPeerBehaviourReporter returns a PeerBehaviourReporter which records all reported -// behaviours in memory. -func NewMockPeerBehaviourReporter() *MockPeerBehaviourReporter { - return &MockPeerBehaviourReporter{ - pb: map[ID][]PeerBehaviour{}, - } -} - -// Report stores the PeerBehaviour produced by the peer identified by peerID. -func (mpbr *MockPeerBehaviourReporter) Report(peerID ID, behaviour PeerBehaviour) { - mpbr.mtx.Lock() - defer mpbr.mtx.Unlock() - mpbr.pb[peerID] = append(mpbr.pb[peerID], behaviour) -} - -// GetBehaviours returns all behaviours reported on the peer identified by peerID. -func (mpbr *MockPeerBehaviourReporter) GetBehaviours(peerID ID) []PeerBehaviour { - mpbr.mtx.RLock() - defer mpbr.mtx.RUnlock() - if items, ok := mpbr.pb[peerID]; ok { - result := make([]PeerBehaviour, len(items)) - copy(result, items) - - return result - } else { - return []PeerBehaviour{} - } -} diff --git a/p2p/peer_behaviour_test.go b/p2p/peer_behaviour_test.go deleted file mode 100644 index 1822844d6..000000000 --- a/p2p/peer_behaviour_test.go +++ /dev/null @@ -1,180 +0,0 @@ -package p2p_test - -import ( - "sync" - "testing" - - "github.com/tendermint/tendermint/p2p" -) - -// TestMockPeerBehaviour tests the MockPeerBehaviour' ability to store reported -// peer behaviour in memory indexed by the peerID -func TestMockPeerBehaviourReporter(t *testing.T) { - var peerID p2p.ID = "MockPeer" - pr := p2p.NewMockPeerBehaviourReporter() - - behaviours := pr.GetBehaviours(peerID) - if len(behaviours) != 0 { - t.Error("Expected to have no behaviours reported") - } - - pr.Report(peerID, p2p.PeerBehaviourBadMessage) - behaviours = pr.GetBehaviours(peerID) - if len(behaviours) != 1 { - t.Error("Expected the peer have one reported behaviour") - } - - if behaviours[0] != p2p.PeerBehaviourBadMessage { - t.Error("Expected PeerBehaviourBadMessage to have been reported") - } -} - -type scriptedBehaviours struct { - PeerID p2p.ID - Behaviours []p2p.PeerBehaviour -} - -type scriptItem struct { - PeerID p2p.ID - Behaviour p2p.PeerBehaviour -} - -// equalBehaviours returns true if a and b contain the same PeerBehaviours with -// the same freequency and otherwise false. -func equalBehaviours(a []p2p.PeerBehaviour, b []p2p.PeerBehaviour) bool { - aHistogram := map[p2p.PeerBehaviour]int{} - bHistogram := map[p2p.PeerBehaviour]int{} - - for _, behaviour := range a { - aHistogram[behaviour] += 1 - } - - for _, behaviour := range b { - bHistogram[behaviour] += 1 - } - - if len(aHistogram) != len(bHistogram) { - return false - } - - for _, behaviour := range a { - if aHistogram[behaviour] != bHistogram[behaviour] { - return false - } - } - - for _, behaviour := range b { - if bHistogram[behaviour] != aHistogram[behaviour] { - return false - } - } - - return true -} - -// TestEqualPeerBehaviours tests that equalBehaviours can tell that two slices -// of peer behaviours can be compared for the behaviours they contain and the -// freequencies that those behaviours occur. -func TestEqualPeerBehaviours(t *testing.T) { - equals := []struct { - left []p2p.PeerBehaviour - right []p2p.PeerBehaviour - }{ - // Empty sets - {[]p2p.PeerBehaviour{}, []p2p.PeerBehaviour{}}, - // Single behaviours - {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote}, []p2p.PeerBehaviour{p2p.PeerBehaviourVote}}, - // Equal Frequencies - {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}, - []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, - // Equal frequencies different orders - {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourBlockPart}, - []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote}}, - } - - for _, test := range equals { - if !equalBehaviours(test.left, test.right) { - t.Errorf("Expected %#v and %#v to be equal", test.left, test.right) - } - } - - unequals := []struct { - left []p2p.PeerBehaviour - right []p2p.PeerBehaviour - }{ - // Comparing empty sets to non empty sets - {[]p2p.PeerBehaviour{}, []p2p.PeerBehaviour{p2p.PeerBehaviourVote}}, - // Different behaviours - {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote}, []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart}}, - // Same behaviour with different frequencies - {[]p2p.PeerBehaviour{p2p.PeerBehaviourVote}, - []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, - } - - for _, test := range unequals { - if equalBehaviours(test.left, test.right) { - t.Errorf("Expected %#v and %#v to be unequal", test.left, test.right) - } - } -} - -// TestPeerBehaviourConcurrency constructs a scenario in which -// multiple goroutines are using the same MockPeerBehaviourReporter instance. -// This test reproduces the conditions in which MockPeerBehaviourReporter will -// be used within a Reactor Receive method tests to ensure thread safety. -func TestMockPeerBehaviourReporterConcurrency(t *testing.T) { - behaviourScript := []scriptedBehaviours{ - {"1", []p2p.PeerBehaviour{p2p.PeerBehaviourVote}}, - {"2", []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, - {"3", []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote, p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote}}, - {"4", []p2p.PeerBehaviour{p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote, p2p.PeerBehaviourVote}}, - {"5", []p2p.PeerBehaviour{p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote, p2p.PeerBehaviourBlockPart, p2p.PeerBehaviourVote}}, - } - - var receiveWg sync.WaitGroup - pr := p2p.NewMockPeerBehaviourReporter() - scriptItems := make(chan scriptItem) - done := make(chan int) - numConsumers := 3 - for i := 0; i < numConsumers; i++ { - receiveWg.Add(1) - go func() { - defer receiveWg.Done() - for { - select { - case pb := <-scriptItems: - pr.Report(pb.PeerID, pb.Behaviour) - case <-done: - return - } - } - }() - } - - var sendingWg sync.WaitGroup - sendingWg.Add(1) - go func() { - defer sendingWg.Done() - for _, item := range behaviourScript { - for _, reason := range item.Behaviours { - scriptItems <- scriptItem{item.PeerID, reason} - } - } - }() - - sendingWg.Wait() - - for i := 0; i < numConsumers; i++ { - done <- 1 - } - - receiveWg.Wait() - - for _, items := range behaviourScript { - reported := pr.GetBehaviours(items.PeerID) - if !equalBehaviours(reported, items.Behaviours) { - t.Errorf("Expected peer %s to have behaved \nExpected: %#v \nGot %#v \n", - items.PeerID, items.Behaviours, reported) - } - } -} From f46ed4aac888ce619b4b2f94fb6998fb5da76a1b Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Fri, 7 Jun 2019 07:22:02 +0000 Subject: [PATCH 058/211] libs/db: Fix the BoltDB Batch.Delete `Delete` should queue a delete operation that targets the entire database, and not just keys that happen to be in the current batch. --- CHANGELOG_PENDING.md | 1 + libs/db/boltdb.go | 44 +++++++++++++++++--------------------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 8a8ad2795..286c1cb02 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -28,3 +28,4 @@ * [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. ### BUG FIXES: +- [libs/db] Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go index 6bf474c86..6b44c5560 100644 --- a/libs/db/boltdb.go +++ b/libs/db/boltdb.go @@ -149,55 +149,45 @@ func (bdb *BoltDB) Stats() map[string]string { // boltDBBatch stores key values in sync.Map and dumps them to the underlying // DB upon Write call. type boltDBBatch struct { - buffer []struct { - k []byte - v []byte - } - db *BoltDB + db *BoltDB + ops []operation } // NewBatch returns a new batch. func (bdb *BoltDB) NewBatch() Batch { return &boltDBBatch{ - buffer: make([]struct { - k []byte - v []byte - }, 0), - db: bdb, + ops: nil, + db: bdb, } } // It is safe to modify the contents of the argument after Set returns but not // before. func (bdb *boltDBBatch) Set(key, value []byte) { - bdb.buffer = append(bdb.buffer, struct { - k []byte - v []byte - }{ - key, value, - }) + bdb.ops = append(bdb.ops, operation{opTypeSet, key, value}) } // It is safe to modify the contents of the argument after Delete returns but // not before. func (bdb *boltDBBatch) Delete(key []byte) { - for i, elem := range bdb.buffer { - if bytes.Equal(elem.k, key) { - // delete without preserving order - bdb.buffer[i] = bdb.buffer[len(bdb.buffer)-1] - bdb.buffer = bdb.buffer[:len(bdb.buffer)-1] - return - } - } + bdb.ops = append(bdb.ops, operation{opTypeDelete, key, nil}) } // NOTE: the operation is synchronous (see BoltDB for reasons) func (bdb *boltDBBatch) Write() { err := bdb.db.db.Batch(func(tx *bbolt.Tx) error { b := tx.Bucket(bucket) - for _, elem := range bdb.buffer { - if putErr := b.Put(elem.k, elem.v); putErr != nil { - return putErr + for _, op := range bdb.ops { + key := nonEmptyKey(nonNilBytes(op.key)) + switch op.opType { + case opTypeSet: + if putErr := b.Put(key, op.value); putErr != nil { + return putErr + } + case opTypeDelete: + if delErr := b.Delete(key); delErr != nil { + return delErr + } } } return nil From 319ecb3005fe319369f3be91db0fc8605ee2706b Mon Sep 17 00:00:00 2001 From: Yawning Angel Date: Fri, 7 Jun 2019 07:26:47 +0000 Subject: [PATCH 059/211] libs/db: Fix the BoltDB Get and Iterator BoltDB's accessors will return slices that are only valid for the lifetime of the transaction. This adds copies where required to prevent hard to debug crashes (among other things). --- CHANGELOG_PENDING.md | 1 + libs/db/boltdb.go | 12 +++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 286c1cb02..f96956e72 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -29,3 +29,4 @@ ### BUG FIXES: - [libs/db] Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) +- [libs/db] Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go index 6b44c5560..30501dd82 100644 --- a/libs/db/boltdb.go +++ b/libs/db/boltdb.go @@ -66,7 +66,9 @@ func (bdb *BoltDB) Get(key []byte) (value []byte) { key = nonEmptyKey(nonNilBytes(key)) err := bdb.db.View(func(tx *bbolt.Tx) error { b := tx.Bucket(bucket) - value = b.Get(key) + if v := b.Get(key); v != nil { + value = append([]byte{}, v...) + } return nil }) if err != nil { @@ -312,12 +314,16 @@ func (itr *boltDBIterator) Next() { func (itr *boltDBIterator) Key() []byte { itr.assertIsValid() - return itr.currentKey + return append([]byte{}, itr.currentKey...) } func (itr *boltDBIterator) Value() []byte { itr.assertIsValid() - return itr.currentValue + var value []byte + if itr.currentValue != nil { + value = append([]byte{}, itr.currentValue...) + } + return value } func (itr *boltDBIterator) Close() { From 8b7ca8fd99615cda2f91c385428fb671943888f9 Mon Sep 17 00:00:00 2001 From: Jack Zampolin Date: Sun, 9 Jun 2019 05:27:48 -0700 Subject: [PATCH 060/211] switch to go mod (#3613) * Update to using go mod from dep * Remove references to make get_vendor_deps * Specify go version * Set GO111MODULE=on and add -mod=readonly * Fix exported env * switch to using go1.12 everywhere * Fix test scripts * Typo: * Prepend GO111MODULE=on * remove dep cache * Revert "remove dep cache" This reverts commit 45117bda Signed-off-by: Ismail Khoffi * bring back the dependency cache and change it to cache modules instead of vendored deps; also: - bump version for dependency cache - bump version on pkg-cache (includes modules directory) Signed-off-by: Ismail Khoffi * remove some more traces of dep: - remove Gopkg.(toml | lock) - update contributing guidlines - set global default in circleci (GO111MODULE=on) Signed-off-by: Ismail Khoffi * global var failed for `test_cover` with `go: unknown environment setting GO111MODULE=true` although the var was `GO111MODULE: on` Signed-off-by: Ismail Khoffi * Changelog pending entry Signed-off-by: Ismail Khoffi * Add bbolt dependency to go.mod Signed-off-by: Ismail Khoffi * move -mod=readonly to build flags --- .circleci/config.yml | 43 +- CHANGELOG_PENDING.md | 5 + CONTRIBUTING.md | 11 +- DOCKER/Dockerfile.develop | 1 - DOCKER/Dockerfile.testing | 2 +- Gopkg.lock | 561 ------------------- Gopkg.toml | 97 ---- Makefile | 29 +- Vagrantfile | 2 +- abci/tests/test_app/test.sh | 6 +- appveyor.yml | 1 - docs/app-dev/abci-cli.md | 1 - docs/app-dev/getting-started.md | 1 - docs/introduction/install.md | 2 - docs/tools/benchmarking.md | 1 - docs/tools/monitoring.md | 1 - go.mod | 52 ++ go.sum | 118 ++++ libs/circle.yml | 2 +- networks/remote/integration.sh | 1 - scripts/dist.sh | 3 - scripts/get_tools.sh | 3 - scripts/install/install_tendermint_arm.sh | 1 - scripts/install/install_tendermint_bsd.sh | 1 - scripts/install/install_tendermint_osx.sh | 1 - scripts/install/install_tendermint_ubuntu.sh | 1 - test/app/counter_test.sh | 4 +- test/docker/Dockerfile | 3 +- tools/build/Makefile | 8 +- tools/tm-bench/Dockerfile.dev | 1 - tools/tm-bench/README.md | 1 - tools/tm-monitor/Dockerfile.dev | 1 - tools/tm-monitor/README.md | 1 - 33 files changed, 221 insertions(+), 745 deletions(-) delete mode 100644 Gopkg.lock delete mode 100644 Gopkg.toml create mode 100644 go.mod create mode 100644 go.sum diff --git a/.circleci/config.yml b/.circleci/config.yml index 7ad793549..04a03eb25 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -26,17 +26,12 @@ jobs: - checkout - restore_cache: keys: - - v3-pkg-cache + - v4-pkg-cache - run: name: tools command: | export PATH="$GOBIN:$PATH" make get_tools - - run: - name: dependencies - command: | - export PATH="$GOBIN:$PATH" - make get_vendor_deps - run: name: binaries command: | @@ -48,7 +43,7 @@ jobs: - bin - profiles - save_cache: - key: v3-pkg-cache + key: v4-pkg-cache paths: - /go/pkg - save_cache: @@ -62,7 +57,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -78,7 +73,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -87,12 +82,6 @@ jobs: set -ex export PATH="$GOBIN:$PATH" make lint - - run: - name: check_dep - command: | - set -ex - export PATH="$GOBIN:$PATH" - make check_dep test_abci_apps: <<: *defaults @@ -100,7 +89,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -117,7 +106,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -132,7 +121,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: sudo apt-get update && sudo apt-get install -y --no-install-recommends bsdmainutils @@ -147,7 +136,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: mkdir -p /tmp/logs @@ -157,7 +146,7 @@ jobs: for pkg in $(go list github.com/tendermint/tendermint/... | circleci tests split --split-by=timings); do id=$(basename "$pkg") - go test -v -timeout 5m -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" | tee "/tmp/logs/$id-$RANDOM.log" + GO111MODULE=on go test -v -timeout 5m -mod=readonly -race -coverprofile=/tmp/workspace/profiles/$id.out -covermode=atomic "$pkg" | tee "/tmp/logs/$id-$RANDOM.log" done - persist_to_workspace: root: /tmp/workspace @@ -172,7 +161,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -219,7 +208,7 @@ jobs: - attach_workspace: at: /tmp/workspace - restore_cache: - key: v3-pkg-cache + key: v4-pkg-cache - restore_cache: key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - run: @@ -278,15 +267,15 @@ jobs: - run: name: Build dependencies command: | - make get_tools get_vendor_deps + make get_tools - persist_to_workspace: root: . paths: - "release-version.source" - save_cache: - key: v1-release-deps-{{ .Branch }}-{{ .Revision }} - paths: - - "vendor" + key: v2-release-deps-{{ checksum "go.sum" }} + paths: + - "/go/pkg/mod" build_artifacts: <<: *defaults @@ -295,7 +284,7 @@ jobs: - checkout - restore_cache: keys: - - v1-release-deps-{{ .Branch }}-{{ .Revision }} + - v2-release-deps-{{ checksum "go.sum" }} - attach_workspace: at: /tmp/workspace - run: diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f96956e72..45ed56919 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -4,6 +4,11 @@ ### BREAKING CHANGES: +- \#3613 Switch from golang/dep to Go 1.11 Modules to resolve dependencies: + - it is recommended to switch to Go Modules if your project has tendermint + as a dependency + - read more on Modules here: https://github.com/golang/go/wiki/Modules + * CLI/RPC/Config - [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` -> `results.deliver_tx`). See docs for details. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 82705f1e8..e68e6d1ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -42,7 +42,7 @@ Please don't make Pull Requests to `master`. ## Dependencies -We use [dep](https://github.com/golang/dep) to manage dependencies. +We use [go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies. That said, the master branch of every Tendermint repository should just build with `go get`, which means they should be kept up-to-date with their @@ -50,18 +50,17 @@ dependencies so we can get away with telling people they can just `go get` our software. Since some dependencies are not under our control, a third party may break our -build, in which case we can fall back on `dep ensure` (or `make -get_vendor_deps`). Even for dependencies under our control, dep helps us to +build, in which case we can fall back on `go mod tidy`. Even for dependencies under our control, go helps us to keep multiple repos in sync as they evolve. Anything with an executable, such as apps, tools, and the core, should use dep. -Run `dep status` to get a list of vendor dependencies that may not be +Run `go list -u -m all` to get a list of dependencies that may not be up-to-date. When updating dependencies, please only update the particular dependencies you -need. Instead of running `dep ensure -update`, which will update anything, +need. Instead of running `go get -u=patch`, which will update anything, specify exactly the dependency you want to update, eg. -`dep ensure -update github.com/tendermint/go-amino`. +`GO111MODULE=on go get -u github.com/tendermint/go-amino@master`. ## Vagrant diff --git a/DOCKER/Dockerfile.develop b/DOCKER/Dockerfile.develop index 5759e7658..943b21291 100644 --- a/DOCKER/Dockerfile.develop +++ b/DOCKER/Dockerfile.develop @@ -19,7 +19,6 @@ RUN mkdir -p /go/src/github.com/tendermint/tendermint && \ git clone https://github.com/tendermint/tendermint . && \ git checkout develop && \ make get_tools && \ - make get_vendor_deps && \ make install && \ cd - && \ rm -rf /go/src/github.com/tendermint/tendermint && \ diff --git a/DOCKER/Dockerfile.testing b/DOCKER/Dockerfile.testing index b82afe2a8..a658aeb10 100644 --- a/DOCKER/Dockerfile.testing +++ b/DOCKER/Dockerfile.testing @@ -1,4 +1,4 @@ -FROM golang:1.10.1 +FROM golang:1.12 # Grab deps (jq, hexdump, xxd, killall) diff --git a/Gopkg.lock b/Gopkg.lock deleted file mode 100644 index 6d9236559..000000000 --- a/Gopkg.lock +++ /dev/null @@ -1,561 +0,0 @@ -# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'. - - -[[projects]] - branch = "master" - digest = "1:d6afaeed1502aa28e80a4ed0981d570ad91b2579193404256ce672ed0a609e0d" - name = "github.com/beorn7/perks" - packages = ["quantile"] - pruneopts = "UT" - revision = "3a771d992973f24aa725d07868b467d1ddfceafb" - -[[projects]] - digest = "1:093bf93a65962e8191e3e8cd8fc6c363f83d43caca9739c906531ba7210a9904" - name = "github.com/btcsuite/btcd" - packages = ["btcec"] - pruneopts = "UT" - revision = "ed77733ec07dfc8a513741138419b8d9d3de9d2d" - -[[projects]] - digest = "1:1d8e1cb71c33a9470bbbae09bfec09db43c6bf358dfcae13cd8807c4e2a9a2bf" - name = "github.com/btcsuite/btcutil" - packages = [ - "base58", - "bech32", - ] - pruneopts = "UT" - revision = "d4cc87b860166d00d6b5b9e0d3b3d71d6088d4d4" - -[[projects]] - digest = "1:ffe9824d294da03b391f44e1ae8281281b4afc1bdaa9588c9097785e3af10cec" - name = "github.com/davecgh/go-spew" - packages = ["spew"] - pruneopts = "UT" - revision = "8991bc29aa16c548c550c7ff78260e27b9ab7c73" - version = "v1.1.1" - -[[projects]] - digest = "1:5f7414cf41466d4b4dd7ec52b2cd3e481e08cfd11e7e24fef730c0e483e88bb1" - name = "github.com/etcd-io/bbolt" - packages = ["."] - pruneopts = "UT" - revision = "63597a96ec0ad9e6d43c3fc81e809909e0237461" - version = "v1.3.2" - -[[projects]] - digest = "1:544229a3ca0fb2dd5ebc2896d3d2ff7ce096d9751635301e44e37e761349ee70" - name = "github.com/fortytw2/leaktest" - packages = ["."] - pruneopts = "UT" - revision = "a5ef70473c97b71626b9abeda80ee92ba2a7de9e" - version = "v1.2.0" - -[[projects]] - digest = "1:abeb38ade3f32a92943e5be54f55ed6d6e3b6602761d74b4aab4c9dd45c18abd" - name = "github.com/fsnotify/fsnotify" - packages = ["."] - pruneopts = "UT" - revision = "c2828203cd70a50dcccfb2761f8b1f8ceef9a8e9" - version = "v1.4.7" - -[[projects]] - digest = "1:fdf5169073fb0ad6dc12a70c249145e30f4058647bea25f0abd48b6d9f228a11" - name = "github.com/go-kit/kit" - packages = [ - "log", - "log/level", - "log/term", - "metrics", - "metrics/discard", - "metrics/internal/lv", - "metrics/prometheus", - ] - pruneopts = "UT" - revision = "4dc7be5d2d12881735283bcab7352178e190fc71" - version = "v0.6.0" - -[[projects]] - digest = "1:31a18dae27a29aa074515e43a443abfd2ba6deb6d69309d8d7ce789c45f34659" - name = "github.com/go-logfmt/logfmt" - packages = ["."] - pruneopts = "UT" - revision = "390ab7935ee28ec6b286364bba9b4dd6410cb3d5" - version = "v0.3.0" - -[[projects]] - digest = "1:586ea76dbd0374d6fb649a91d70d652b7fe0ccffb8910a77468e7702e7901f3d" - name = "github.com/go-stack/stack" - packages = ["."] - pruneopts = "UT" - revision = "2fee6af1a9795aafbe0253a0cfbdf668e1fb8a9a" - version = "v1.8.0" - -[[projects]] - digest = "1:95e1006e41c641abd2f365dfa0f1213c04da294e7cd5f0bf983af234b775db64" - name = "github.com/gogo/protobuf" - packages = [ - "gogoproto", - "jsonpb", - "proto", - "protoc-gen-gogo/descriptor", - "sortkeys", - "types", - ] - pruneopts = "UT" - revision = "ba06b47c162d49f2af050fb4c75bcbc86a159d5c" - version = "v1.2.1" - -[[projects]] - digest = "1:239c4c7fd2159585454003d9be7207167970194216193a8a210b8d29576f19c9" - name = "github.com/golang/protobuf" - packages = [ - "proto", - "ptypes", - "ptypes/any", - "ptypes/duration", - "ptypes/timestamp", - ] - pruneopts = "UT" - revision = "c823c79ea1570fb5ff454033735a8e68575d1d0f" - version = "v1.3.0" - -[[projects]] - branch = "master" - digest = "1:4a0c6bb4805508a6287675fac876be2ac1182539ca8a32468d8128882e9d5009" - name = "github.com/golang/snappy" - packages = ["."] - pruneopts = "UT" - revision = "2e65f85255dbc3072edf28d6b5b8efc472979f5a" - -[[projects]] - digest = "1:43dd08a10854b2056e615d1b1d22ac94559d822e1f8b6fcc92c1a1057e85188e" - name = "github.com/gorilla/websocket" - packages = ["."] - pruneopts = "UT" - revision = "ea4d1f681babbce9545c9c5f3d5194a789c89f5b" - version = "v1.2.0" - -[[projects]] - digest = "1:ea40c24cdbacd054a6ae9de03e62c5f252479b96c716375aace5c120d68647c8" - name = "github.com/hashicorp/hcl" - packages = [ - ".", - "hcl/ast", - "hcl/parser", - "hcl/scanner", - "hcl/strconv", - "hcl/token", - "json/parser", - "json/scanner", - "json/token", - ] - pruneopts = "UT" - revision = "8cb6e5b959231cc1119e43259c4a608f9c51a241" - version = "v1.0.0" - -[[projects]] - digest = "1:870d441fe217b8e689d7949fef6e43efbc787e50f200cb1e70dbca9204a1d6be" - name = "github.com/inconshreveable/mousetrap" - packages = ["."] - pruneopts = "UT" - revision = "76626ae9c91c4f2a10f34cad8ce83ea42c93bb75" - version = "v1.0" - -[[projects]] - digest = "1:a74b5a8e34ee5843cd6e65f698f3e75614f812ff170c2243425d75bc091e9af2" - name = "github.com/jmhodges/levigo" - packages = ["."] - pruneopts = "UT" - revision = "853d788c5c416eaaee5b044570784a96c7a26975" - version = "v1.0.0" - -[[projects]] - branch = "master" - digest = "1:a64e323dc06b73892e5bb5d040ced475c4645d456038333883f58934abbf6f72" - name = "github.com/kr/logfmt" - packages = ["."] - pruneopts = "UT" - revision = "b84e30acd515aadc4b783ad4ff83aff3299bdfe0" - -[[projects]] - digest = "1:53e8c5c79716437e601696140e8b1801aae4204f4ec54a504333702a49572c4f" - name = "github.com/magiconair/properties" - packages = [ - ".", - "assert", - ] - pruneopts = "UT" - revision = "c2353362d570a7bfa228149c62842019201cfb71" - version = "v1.8.0" - -[[projects]] - digest = "1:ff5ebae34cfbf047d505ee150de27e60570e8c394b3b8fdbb720ff6ac71985fc" - name = "github.com/matttproud/golang_protobuf_extensions" - packages = ["pbutil"] - pruneopts = "UT" - revision = "c12348ce28de40eed0136aa2b644d0ee0650e56c" - version = "v1.0.1" - -[[projects]] - digest = "1:53bc4cd4914cd7cd52139990d5170d6dc99067ae31c56530621b18b35fc30318" - name = "github.com/mitchellh/mapstructure" - packages = ["."] - pruneopts = "UT" - revision = "3536a929edddb9a5b34bd6861dc4a9647cb459fe" - version = "v1.1.2" - -[[projects]] - digest = "1:95741de3af260a92cc5c7f3f3061e85273f5a81b5db20d4bd68da74bd521675e" - name = "github.com/pelletier/go-toml" - packages = ["."] - pruneopts = "UT" - revision = "c01d1270ff3e442a8a57cddc1c92dc1138598194" - version = "v1.2.0" - -[[projects]] - digest = "1:40e195917a951a8bf867cd05de2a46aaf1806c50cf92eebf4c16f78cd196f747" - name = "github.com/pkg/errors" - packages = ["."] - pruneopts = "UT" - revision = "645ef00459ed84a119197bfb8d8205042c6df63d" - version = "v0.8.0" - -[[projects]] - digest = "1:0028cb19b2e4c3112225cd871870f2d9cf49b9b4276531f03438a88e94be86fe" - name = "github.com/pmezard/go-difflib" - packages = ["difflib"] - pruneopts = "UT" - revision = "792786c7400a136282c1664665ae0a8db921c6c2" - version = "v1.0.0" - -[[projects]] - digest = "1:26663fafdea73a38075b07e8e9d82fc0056379d2be8bb4e13899e8fda7c7dd23" - name = "github.com/prometheus/client_golang" - packages = [ - "prometheus", - "prometheus/internal", - "prometheus/promhttp", - ] - pruneopts = "UT" - revision = "abad2d1bd44235a26707c172eab6bca5bf2dbad3" - version = "v0.9.1" - -[[projects]] - branch = "master" - digest = "1:2d5cd61daa5565187e1d96bae64dbbc6080dacf741448e9629c64fd93203b0d4" - name = "github.com/prometheus/client_model" - packages = ["go"] - pruneopts = "UT" - revision = "5c3871d89910bfb32f5fcab2aa4b9ec68e65a99f" - -[[projects]] - branch = "master" - digest = "1:db712fde5d12d6cdbdf14b777f0c230f4ff5ab0be8e35b239fc319953ed577a4" - name = "github.com/prometheus/common" - packages = [ - "expfmt", - "internal/bitbucket.org/ww/goautoneg", - "model", - ] - pruneopts = "UT" - revision = "7e9e6cabbd393fc208072eedef99188d0ce788b6" - -[[projects]] - branch = "master" - digest = "1:ef74914912f99c79434d9c09658274678bc85080ebe3ab32bec3940ebce5e1fc" - name = "github.com/prometheus/procfs" - packages = [ - ".", - "internal/util", - "nfs", - "xfs", - ] - pruneopts = "UT" - revision = "185b4288413d2a0dd0806f78c90dde719829e5ae" - -[[projects]] - digest = "1:c4556a44e350b50a490544d9b06e9fba9c286c21d6c0e47f54f3a9214597298c" - name = "github.com/rcrowley/go-metrics" - packages = ["."] - pruneopts = "UT" - revision = "e2704e165165ec55d062f5919b4b29494e9fa790" - -[[projects]] - digest = "1:b0c25f00bad20d783d259af2af8666969e2fc343fa0dc9efe52936bbd67fb758" - name = "github.com/rs/cors" - packages = ["."] - pruneopts = "UT" - revision = "9a47f48565a795472d43519dd49aac781f3034fb" - version = "v1.6.0" - -[[projects]] - digest = "1:6a4a11ba764a56d2758899ec6f3848d24698d48442ebce85ee7a3f63284526cd" - name = "github.com/spf13/afero" - packages = [ - ".", - "mem", - ] - pruneopts = "UT" - revision = "d40851caa0d747393da1ffb28f7f9d8b4eeffebd" - version = "v1.1.2" - -[[projects]] - digest = "1:08d65904057412fc0270fc4812a1c90c594186819243160dc779a402d4b6d0bc" - name = "github.com/spf13/cast" - packages = ["."] - pruneopts = "UT" - revision = "8c9545af88b134710ab1cd196795e7f2388358d7" - version = "v1.3.0" - -[[projects]] - digest = "1:7ffc0983035bc7e297da3688d9fe19d60a420e9c38bef23f845c53788ed6a05e" - name = "github.com/spf13/cobra" - packages = ["."] - pruneopts = "UT" - revision = "7b2c5ac9fc04fc5efafb60700713d4fa609b777b" - version = "v0.0.1" - -[[projects]] - digest = "1:68ea4e23713989dc20b1bded5d9da2c5f9be14ff9885beef481848edd18c26cb" - name = "github.com/spf13/jwalterweatherman" - packages = ["."] - pruneopts = "UT" - revision = "4a4406e478ca629068e7768fc33f3f044173c0a6" - version = "v1.0.0" - -[[projects]] - digest = "1:c1b1102241e7f645bc8e0c22ae352e8f0dc6484b6cb4d132fa9f24174e0119e2" - name = "github.com/spf13/pflag" - packages = ["."] - pruneopts = "UT" - revision = "298182f68c66c05229eb03ac171abe6e309ee79a" - version = "v1.0.3" - -[[projects]] - digest = "1:f8e1a678a2571e265f4bf91a3e5e32aa6b1474a55cb0ea849750cc177b664d96" - name = "github.com/spf13/viper" - packages = ["."] - pruneopts = "UT" - revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7" - version = "v1.0.0" - -[[projects]] - digest = "1:7e8d267900c7fa7f35129a2a37596e38ed0f11ca746d6d9ba727980ee138f9f6" - name = "github.com/stretchr/testify" - packages = [ - "assert", - "require", - ] - pruneopts = "UT" - revision = "12b6f73e6084dad08a7c6e575284b177ecafbc71" - version = "v1.2.1" - -[[projects]] - branch = "master" - digest = "1:59483b8e8183f10ab21a85ba1f4cbb4a2335d48891801f79ed7b9499f44d383c" - name = "github.com/syndtr/goleveldb" - packages = [ - "leveldb", - "leveldb/cache", - "leveldb/comparer", - "leveldb/errors", - "leveldb/filter", - "leveldb/iterator", - "leveldb/journal", - "leveldb/memdb", - "leveldb/opt", - "leveldb/storage", - "leveldb/table", - "leveldb/util", - ] - pruneopts = "UT" - revision = "6b91fda63f2e36186f1c9d0e48578defb69c5d43" - -[[projects]] - digest = "1:ad9c4c1a4e7875330b1f62906f2830f043a23edb5db997e3a5ac5d3e6eadf80a" - name = "github.com/tendermint/go-amino" - packages = ["."] - pruneopts = "UT" - revision = "dc14acf9ef15f85828bfbc561ed9dd9d2a284885" - version = "v0.14.1" - -[[projects]] - branch = "master" - digest = "1:f4edb30d5ff238e2abba10457010f74cd55ae20bbda8c54db1a07155fa020490" - name = "golang.org/x/crypto" - packages = [ - "bcrypt", - "blowfish", - "chacha20poly1305", - "curve25519", - "ed25519", - "ed25519/internal/edwards25519", - "hkdf", - "internal/chacha20", - "internal/subtle", - "nacl/box", - "nacl/secretbox", - "openpgp/armor", - "openpgp/errors", - "poly1305", - "ripemd160", - "salsa20/salsa", - ] - pruneopts = "UT" - revision = "8dd112bcdc25174059e45e07517d9fc663123347" - -[[projects]] - digest = "1:d36f55a999540d29b6ea3c2ea29d71c76b1d9853fdcd3e5c5cb4836f2ba118f1" - name = "golang.org/x/net" - packages = [ - "context", - "http/httpguts", - "http2", - "http2/hpack", - "idna", - "internal/timeseries", - "netutil", - "trace", - ] - pruneopts = "UT" - revision = "292b43bbf7cb8d35ddf40f8d5100ef3837cced3f" - -[[projects]] - branch = "master" - digest = "1:6f86e2f2e2217cd4d74dec6786163cf80e4d2b99adb341ecc60a45113b844dca" - name = "golang.org/x/sys" - packages = [ - "cpu", - "unix", - ] - pruneopts = "UT" - revision = "7e31e0c00fa05cb5fbf4347b585621d6709e19a4" - -[[projects]] - digest = "1:a2ab62866c75542dd18d2b069fec854577a20211d7c0ea6ae746072a1dccdd18" - name = "golang.org/x/text" - packages = [ - "collate", - "collate/build", - "internal/colltab", - "internal/gen", - "internal/tag", - "internal/triegen", - "internal/ucd", - "language", - "secure/bidirule", - "transform", - "unicode/bidi", - "unicode/cldr", - "unicode/norm", - "unicode/rangetable", - ] - pruneopts = "UT" - revision = "f21a4dfb5e38f5895301dc265a8def02365cc3d0" - version = "v0.3.0" - -[[projects]] - branch = "master" - digest = "1:56b0bca90b7e5d1facf5fbdacba23e4e0ce069d25381b8e2f70ef1e7ebfb9c1a" - name = "google.golang.org/genproto" - packages = ["googleapis/rpc/status"] - pruneopts = "UT" - revision = "b69ba1387ce2108ac9bc8e8e5e5a46e7d5c72313" - -[[projects]] - digest = "1:2dab32a43451e320e49608ff4542fdfc653c95dcc35d0065ec9c6c3dd540ed74" - name = "google.golang.org/grpc" - packages = [ - ".", - "balancer", - "balancer/base", - "balancer/roundrobin", - "codes", - "connectivity", - "credentials", - "encoding", - "encoding/proto", - "grpclog", - "internal", - "internal/backoff", - "internal/channelz", - "internal/grpcrand", - "keepalive", - "metadata", - "naming", - "peer", - "resolver", - "resolver/dns", - "resolver/passthrough", - "stats", - "status", - "tap", - "transport", - ] - pruneopts = "UT" - revision = "168a6198bcb0ef175f7dacec0b8691fc141dc9b8" - version = "v1.13.0" - -[[projects]] - digest = "1:342378ac4dcb378a5448dd723f0784ae519383532f5e70ade24132c4c8693202" - name = "gopkg.in/yaml.v2" - packages = ["."] - pruneopts = "UT" - revision = "5420a8b6744d3b0345ab293f6fcba19c978f1183" - version = "v2.2.1" - -[solve-meta] - analyzer-name = "dep" - analyzer-version = 1 - input-imports = [ - "github.com/btcsuite/btcd/btcec", - "github.com/btcsuite/btcutil/base58", - "github.com/btcsuite/btcutil/bech32", - "github.com/etcd-io/bbolt", - "github.com/fortytw2/leaktest", - "github.com/go-kit/kit/log", - "github.com/go-kit/kit/log/level", - "github.com/go-kit/kit/log/term", - "github.com/go-kit/kit/metrics", - "github.com/go-kit/kit/metrics/discard", - "github.com/go-kit/kit/metrics/prometheus", - "github.com/go-logfmt/logfmt", - "github.com/gogo/protobuf/gogoproto", - "github.com/gogo/protobuf/jsonpb", - "github.com/gogo/protobuf/proto", - "github.com/gogo/protobuf/types", - "github.com/golang/protobuf/proto", - "github.com/golang/protobuf/ptypes/timestamp", - "github.com/gorilla/websocket", - "github.com/jmhodges/levigo", - "github.com/magiconair/properties/assert", - "github.com/pkg/errors", - "github.com/prometheus/client_golang/prometheus", - "github.com/prometheus/client_golang/prometheus/promhttp", - "github.com/rcrowley/go-metrics", - "github.com/rs/cors", - "github.com/spf13/cobra", - "github.com/spf13/viper", - "github.com/stretchr/testify/assert", - "github.com/stretchr/testify/require", - "github.com/syndtr/goleveldb/leveldb", - "github.com/syndtr/goleveldb/leveldb/errors", - "github.com/syndtr/goleveldb/leveldb/iterator", - "github.com/syndtr/goleveldb/leveldb/opt", - "github.com/tendermint/go-amino", - "golang.org/x/crypto/bcrypt", - "golang.org/x/crypto/chacha20poly1305", - "golang.org/x/crypto/curve25519", - "golang.org/x/crypto/ed25519", - "golang.org/x/crypto/hkdf", - "golang.org/x/crypto/nacl/box", - "golang.org/x/crypto/nacl/secretbox", - "golang.org/x/crypto/openpgp/armor", - "golang.org/x/crypto/ripemd160", - "golang.org/x/net/context", - "golang.org/x/net/netutil", - "google.golang.org/grpc", - "google.golang.org/grpc/credentials", - ] - solver-name = "gps-cdcl" - solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml deleted file mode 100644 index e5abf051c..000000000 --- a/Gopkg.toml +++ /dev/null @@ -1,97 +0,0 @@ -# Gopkg.toml example -# -# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md -# for detailed Gopkg.toml documentation. -# -# required = ["github.com/user/thing/cmd/thing"] -# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"] -# -# [[constraint]] -# name = "github.com/user/project" -# version = "1.0.0" -# -# [[override]] -# name = "github.com/x/y" -# version = "2.4.0" -# -# [prune] -# non-go = false -# go-tests = true -# unused-packages = true -# -########################################################### - -# Allow only patch releases for serialization libraries -[[constraint]] - name = "github.com/etcd-io/bbolt" - version = "v1.3.2" - -[[constraint]] - name = "github.com/tendermint/go-amino" - version = "~0.14.1" - -[[constraint]] - name = "github.com/gogo/protobuf" - version = "~1.2.1" - -[[constraint]] - name = "github.com/golang/protobuf" - version = "~1.3.0" - -# Allow only minor releases for other libraries -[[constraint]] - name = "github.com/go-kit/kit" - version = "^0.6.0" - -[[constraint]] - name = "github.com/gorilla/websocket" - version = "^1.2.0" - -[[constraint]] - name = "github.com/rs/cors" - version = "^1.6.0" - -[[constraint]] - name = "github.com/pkg/errors" - version = "^0.8.0" - -[[constraint]] - name = "github.com/spf13/cobra" - version = "^0.0.1" - -[[constraint]] - name = "github.com/spf13/viper" - version = "^1.0.0" - -[[constraint]] - name = "github.com/stretchr/testify" - version = "^1.2.1" - -[[constraint]] - name = "google.golang.org/grpc" - version = "^1.13.0" - -[[constraint]] - name = "github.com/fortytw2/leaktest" - version = "^1.2.0" - -[[constraint]] - name = "github.com/prometheus/client_golang" - version = "^0.9.1" - -[[constraint]] - name = "github.com/jmhodges/levigo" - version = "^1.0.0" - -################################### -## Repos which don't have releases. - -## - github.com/btcsuite/btcd -## - golang.org/x/crypto -## - github.com/btcsuite/btcutil -## - github.com/rcrowley/go-metrics -## - golang.org/x/net - -[prune] - go-tests = true - unused-packages = true diff --git a/Makefile b/Makefile index c5bde692d..1980ac861 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,5 @@ GOTOOLS = \ github.com/mitchellh/gox \ - github.com/golang/dep/cmd/dep \ github.com/golangci/golangci-lint/cmd/golangci-lint \ github.com/gogo/protobuf/protoc-gen-gogo \ github.com/square/certstrap @@ -8,13 +7,15 @@ GOBIN?=${GOPATH}/bin PACKAGES=$(shell go list ./...) OUTPUT?=build/tendermint +export GO111MODULE = on + INCLUDE = -I=. -I=${GOPATH}/src -I=${GOPATH}/src/github.com/gogo/protobuf/protobuf BUILD_TAGS?='tendermint' -BUILD_FLAGS = -ldflags "-X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD`" +BUILD_FLAGS = -mod=readonly -ldflags "-X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD`" all: check build test install -check: check_tools get_vendor_deps +check: check_tools ######################################## ### Build Tendermint @@ -29,10 +30,10 @@ build_race: CGO_ENABLED=0 go build -race $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o $(OUTPUT) ./cmd/tendermint install: - CGO_ENABLED=0 go install $(BUILD_FLAGS) -tags $(BUILD_TAGS) ./cmd/tendermint + CGO_ENABLED=0 go install $(BUILD_FLAGS) -tags $(BUILD_TAGS) ./cmd/tendermint install_c: - CGO_ENABLED=1 go install $(BUILD_FLAGS) -tags "$(BUILD_TAGS) cleveldb" ./cmd/tendermint + CGO_ENABLED=1 go install $(BUILD_FLAGS) -tags "$(BUILD_TAGS) cleveldb" ./cmd/tendermint ######################################## ### Protobuf @@ -56,10 +57,10 @@ protoc_abci: abci/types/types.pb.go protoc_proto3types: types/proto3/block.pb.go build_abci: - @go build -i ./abci/cmd/... + @go build -mod=readonly -i ./abci/cmd/... install_abci: - @go install ./abci/cmd/... + @go install -mod=readonly ./abci/cmd/... ######################################## ### Distribution @@ -85,11 +86,6 @@ update_tools: @echo "--> Updating tools" ./scripts/get_tools.sh -#Update dependencies -get_vendor_deps: - @echo "--> Running dep" - @dep ensure - #For ABCI and libs get_protoc: @# https://github.com/google/protobuf/releases @@ -192,7 +188,6 @@ test_p2p: test_integrations: make build_docker_test_image make get_tools - make get_vendor_deps make install make test_cover make test_apps @@ -254,10 +249,6 @@ rpc-docs: cat rpc/core/slate_header.txt > $(DESTINATION) godoc2md -template rpc/core/doc_template.txt github.com/tendermint/tendermint/rpc/core | grep -v -e "pipe.go" -e "routes.go" -e "dev.go" | sed 's,/src/target,https://github.com/tendermint/tendermint/tree/master/rpc/core,' >> $(DESTINATION) -check_dep: - dep status >> /dev/null - !(grep -n branch Gopkg.toml) - ########################################################### ### Docker image @@ -270,7 +261,7 @@ build-docker: ### Local testnet using docker # Build linux binary on other platforms -build-linux: get_tools get_vendor_deps +build-linux: get_tools GOOS=linux GOARCH=amd64 $(MAKE) build build-docker-localnode: @@ -312,4 +303,4 @@ build-slate: # To avoid unintended conflicts with file names, always add to .PHONY # unless there is a reason not to. # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: check build build_race build_abci dist install install_abci check_dep check_tools get_tools update_tools get_vendor_deps draw_deps get_protoc protoc_abci protoc_libs gen_certs clean_certs grpc_dbserver test_cover test_apps test_persistence test_p2p test test_race test_integrations test_release test100 vagrant_test fmt rpc-docs build-linux localnet-start localnet-stop build-docker build-docker-localnode sentry-start sentry-config sentry-stop build-slate protoc_grpc protoc_all build_c install_c test_with_deadlock cleanup_after_test_with_deadlock lint +.PHONY: check build build_race build_abci dist install install_abci check_tools get_tools update_tools draw_deps get_protoc protoc_abci protoc_libs gen_certs clean_certs grpc_dbserver test_cover test_apps test_persistence test_p2p test test_race test_integrations test_release test100 vagrant_test fmt rpc-docs build-linux localnet-start localnet-stop build-docker build-docker-localnode sentry-start sentry-config sentry-stop build-slate protoc_grpc protoc_all build_c install_c test_with_deadlock cleanup_after_test_with_deadlock lint diff --git a/Vagrantfile b/Vagrantfile index da4f8ac3d..67de74297 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -57,6 +57,6 @@ Vagrant.configure("2") do |config| # get all deps and tools, ready to install/test su - vagrant -c 'source /home/vagrant/.bash_profile' - su - vagrant -c 'cd /home/vagrant/go/src/github.com/tendermint/tendermint && make get_tools && make get_vendor_deps' + su - vagrant -c 'cd /home/vagrant/go/src/github.com/tendermint/tendermint && make get_tools' SHELL end diff --git a/abci/tests/test_app/test.sh b/abci/tests/test_app/test.sh index 230c94163..c0bdace27 100755 --- a/abci/tests/test_app/test.sh +++ b/abci/tests/test_app/test.sh @@ -3,6 +3,8 @@ set -e # These tests spawn the counter app and server by execing the ABCI_APP command and run some simple client tests against it +export GO111MODULE=on + # Get the directory of where this script is. SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done @@ -13,13 +15,13 @@ cd "$DIR" echo "RUN COUNTER OVER SOCKET" # test golang counter -ABCI_APP="counter" go run ./*.go +ABCI_APP="counter" go run -mod=readonly ./*.go echo "----------------------" echo "RUN COUNTER OVER GRPC" # test golang counter via grpc -ABCI_APP="counter --abci=grpc" ABCI="grpc" go run ./*.go +ABCI_APP="counter --abci=grpc" ABCI="grpc" go run -mod=readonly ./*.go echo "----------------------" # test nodejs counter diff --git a/appveyor.yml b/appveyor.yml index 1ddf8fdd2..4aa8c2abb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,6 @@ clone_folder: c:\go\path\src\github.com\tendermint\tendermint before_build: - cmd: set GOPATH=%GOROOT%\path - cmd: set PATH=%GOPATH%\bin;%PATH% -- cmd: make get_vendor_deps build_script: - cmd: make test test: off diff --git a/docs/app-dev/abci-cli.md b/docs/app-dev/abci-cli.md index 3e6cced87..b09b9a11b 100644 --- a/docs/app-dev/abci-cli.md +++ b/docs/app-dev/abci-cli.md @@ -16,7 +16,6 @@ cd $GOPATH/src/github.com/tendermint git clone https://github.com/tendermint/tendermint.git cd tendermint make get_tools -make get_vendor_deps make install_abci ``` diff --git a/docs/app-dev/getting-started.md b/docs/app-dev/getting-started.md index 5509a7012..9110761e2 100644 --- a/docs/app-dev/getting-started.md +++ b/docs/app-dev/getting-started.md @@ -28,7 +28,6 @@ Then run go get github.com/tendermint/tendermint cd $GOPATH/src/github.com/tendermint/tendermint make get_tools -make get_vendor_deps make install_abci ``` diff --git a/docs/introduction/install.md b/docs/introduction/install.md index e96135826..4f35ffef7 100644 --- a/docs/introduction/install.md +++ b/docs/introduction/install.md @@ -29,7 +29,6 @@ cd tendermint ``` make get_tools -make get_vendor_deps ``` ### Compile @@ -71,7 +70,6 @@ To upgrade, run ``` cd $GOPATH/src/github.com/tendermint/tendermint git pull origin master -make get_vendor_deps make install ``` diff --git a/docs/tools/benchmarking.md b/docs/tools/benchmarking.md index 67a472e4b..a30ab54ab 100644 --- a/docs/tools/benchmarking.md +++ b/docs/tools/benchmarking.md @@ -75,6 +75,5 @@ Each of the connections is handled via two separate goroutines. ## Development ``` -make get_vendor_deps make test ``` diff --git a/docs/tools/monitoring.md b/docs/tools/monitoring.md index fa3901dde..26b90ed70 100644 --- a/docs/tools/monitoring.md +++ b/docs/tools/monitoring.md @@ -88,6 +88,5 @@ websocket. ``` make get_tools -make get_vendor_deps make test ``` diff --git a/go.mod b/go.mod new file mode 100644 index 000000000..8fe1a124b --- /dev/null +++ b/go.mod @@ -0,0 +1,52 @@ +module github.com/tendermint/tendermint + +go 1.12 + +require ( + github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 + github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d + github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a + github.com/davecgh/go-spew v1.1.1 + github.com/etcd-io/bbolt v1.3.2 + github.com/fortytw2/leaktest v1.2.0 + github.com/fsnotify/fsnotify v1.4.7 + github.com/go-kit/kit v0.6.0 + github.com/go-logfmt/logfmt v0.3.0 + github.com/go-stack/stack v1.8.0 + github.com/gogo/protobuf v1.2.1 + github.com/golang/protobuf v1.3.0 + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db + github.com/gorilla/websocket v1.2.0 + github.com/hashicorp/hcl v1.0.0 + github.com/inconshreveable/mousetrap v1.0.0 + github.com/jmhodges/levigo v1.0.0 + github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 + github.com/magiconair/properties v1.8.0 + github.com/matttproud/golang_protobuf_extensions v1.0.1 + github.com/mitchellh/mapstructure v1.1.2 + github.com/pelletier/go-toml v1.2.0 + github.com/pkg/errors v0.8.0 + github.com/pmezard/go-difflib v1.0.0 + github.com/prometheus/client_golang v0.9.1 + github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 + github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 + github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d + github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 + github.com/rs/cors v1.6.0 + github.com/spf13/afero v1.1.2 + github.com/spf13/cast v1.3.0 + github.com/spf13/cobra v0.0.1 + github.com/spf13/jwalterweatherman v1.0.0 + github.com/spf13/pflag v1.0.3 + github.com/spf13/viper v1.0.0 + github.com/stretchr/testify v1.2.2 + github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e + github.com/tendermint/go-amino v0.14.1 + golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 + golang.org/x/net v0.0.0-20180906233101-161cd47e91fd + golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a + golang.org/x/text v0.3.0 + google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 + google.golang.org/grpc v1.13.0 + gopkg.in/yaml.v2 v2.2.1 +) diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..10e54c0fd --- /dev/null +++ b/go.sum @@ -0,0 +1,118 @@ +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= +github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= +github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= +github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/etcd-io/bbolt v1.3.2 h1:RLRQ0TKLX7DlBRXAJHvbmXL17Q3KNnTBtZ9B6Qo+/Y0= +github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-kit/kit v0.6.0 h1:wTifptAGIyIuir4bRyN4h7+kAa2a4eepLYVmRe5qqQ8= +github.com/go-kit/kit v0.6.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= +github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= +github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= +github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= +github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= +github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 h1:Cto4X6SVMWRPBkJ/3YHn1iDGDGc/Z+sW+AEMKHMVvN4= +github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk= +github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= +github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= +github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= +github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.1 h1:zZh3X5aZbdnoj+4XkaBxKfhO4ot82icYdhhREIAXIj8= +github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= +github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.0 h1:RUA/ghS2i64rlnn4ydTfblY8Og8QzcPtCcHvgMn+w/I= +github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= +github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e h1:91EeXI4y4ShkyzkMqZ7QP/ZTIqwXp3RuDu5WFzxcFAs= +github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= +github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= +golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/net v0.0.0-20180710023853-292b43bbf7cb/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 h1:67iHsV9djwGdZpdZNbLuQj6FOzCaZe3w+vhLjn5AcFA= +google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/grpc v1.13.0 h1:bHIbVsCwmvbArgCJmLdgOdHFXlKqTOVjbibbS19cXHc= +google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/libs/circle.yml b/libs/circle.yml index 390ffb039..2b7d1266c 100644 --- a/libs/circle.yml +++ b/libs/circle.yml @@ -15,7 +15,7 @@ dependencies: test: override: - - cd $PROJECT_PATH && make get_tools && make get_vendor_deps && bash ./test.sh + - cd $PROJECT_PATH && make get_tools && bash ./test.sh post: - cd "$PROJECT_PATH" && bash <(curl -s https://codecov.io/bash) -f coverage.txt - cd "$PROJECT_PATH" && mv coverage.txt "${CIRCLE_ARTIFACTS}" diff --git a/networks/remote/integration.sh b/networks/remote/integration.sh index fb1b0b327..c2d7c3a36 100644 --- a/networks/remote/integration.sh +++ b/networks/remote/integration.sh @@ -31,7 +31,6 @@ cd $GOPATH/src/$REPO ## build make get_tools -make get_vendor_deps make build # generate an ssh key diff --git a/scripts/dist.sh b/scripts/dist.sh index f999c5376..ac62f1099 100755 --- a/scripts/dist.sh +++ b/scripts/dist.sh @@ -31,9 +31,6 @@ XC_EXCLUDE=${XC_EXCLUDE:-" darwin/arm solaris/amd64 solaris/386 solaris/arm free # Make sure build tools are available. make get_tools -# Get VENDORED dependencies -make get_vendor_deps - # Build! # ldflags: -s Omit the symbol table and debug information. # -w Omit the DWARF symbol table. diff --git a/scripts/get_tools.sh b/scripts/get_tools.sh index dd9566917..d8c17df11 100755 --- a/scripts/get_tools.sh +++ b/scripts/get_tools.sh @@ -48,9 +48,6 @@ installFromGithub() { echo "" } -######################## COMMON TOOLS ######################################## -installFromGithub golang/dep 22125cfaa6ddc71e145b1535d4b7ee9744fefff2 cmd/dep - ######################## DEVELOPER TOOLS ##################################### installFromGithub gogo/protobuf 61dbc136cf5d2f08d68a011382652244990a53a9 protoc-gen-gogo diff --git a/scripts/install/install_tendermint_arm.sh b/scripts/install/install_tendermint_arm.sh index b260d8d07..085ba82f4 100644 --- a/scripts/install/install_tendermint_arm.sh +++ b/scripts/install/install_tendermint_arm.sh @@ -32,7 +32,6 @@ git checkout $BRANCH # XXX: uncomment if branch isn't master # git fetch origin $BRANCH make get_tools -make get_vendor_deps make install # the binary is located in $GOPATH/bin diff --git a/scripts/install/install_tendermint_bsd.sh b/scripts/install/install_tendermint_bsd.sh index b76b94855..294155d0e 100644 --- a/scripts/install/install_tendermint_bsd.sh +++ b/scripts/install/install_tendermint_bsd.sh @@ -47,7 +47,6 @@ cd "$GOPATH/src/$REPO" # build & install master git checkout $BRANCH gmake get_tools -gmake get_vendor_deps gmake install # the binary is located in $GOPATH/bin diff --git a/scripts/install/install_tendermint_osx.sh b/scripts/install/install_tendermint_osx.sh index b4107ab01..ee799f66a 100644 --- a/scripts/install/install_tendermint_osx.sh +++ b/scripts/install/install_tendermint_osx.sh @@ -37,5 +37,4 @@ git checkout $BRANCH # XXX: uncomment if branch isn't master # git fetch origin $BRANCH make get_tools -make get_vendor_deps make install diff --git a/scripts/install/install_tendermint_ubuntu.sh b/scripts/install/install_tendermint_ubuntu.sh index 3fe6ea8ed..2e5558ff6 100644 --- a/scripts/install/install_tendermint_ubuntu.sh +++ b/scripts/install/install_tendermint_ubuntu.sh @@ -41,7 +41,6 @@ git checkout $BRANCH # XXX: uncomment if branch isn't master # git fetch origin $BRANCH make get_tools -make get_vendor_deps make install # the binary is located in $GOPATH/bin diff --git a/test/app/counter_test.sh b/test/app/counter_test.sh index 868f8d037..a4f7c83b9 100755 --- a/test/app/counter_test.sh +++ b/test/app/counter_test.sh @@ -1,5 +1,7 @@ #! /bin/bash +export GO111MODULE=on + if [[ "$GRPC_BROADCAST_TX" == "" ]]; then GRPC_BROADCAST_TX="" fi @@ -38,7 +40,7 @@ if [[ "$GRPC_BROADCAST_TX" != "" ]]; then rm grpc_client fi echo "... building grpc_client" - go build -o grpc_client grpc_client.go + go build -mod=readonly -o grpc_client grpc_client.go fi function sendTx() { diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 1a64d4173..77cc515e5 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.10 +FROM golang:1.12 # Add testing deps for curl RUN echo 'deb http://httpredir.debian.org/debian testing main non-free contrib' >> /etc/apt/sources.list @@ -20,7 +20,6 @@ COPY . $REPO # Install the vendored dependencies # docker caching prevents reinstall on code change! RUN make get_tools -RUN make get_vendor_deps # install ABCI CLI RUN make install_abci diff --git a/tools/build/Makefile b/tools/build/Makefile index f9384ac64..8c33ffd5d 100644 --- a/tools/build/Makefile +++ b/tools/build/Makefile @@ -64,7 +64,7 @@ build-tendermint: git-branch gopath-setup @echo "*** Building tendermint" go get -d -u github.com/tendermint/tendermint/cmd/tendermint cd $(GOPATH)/src/github.com/tendermint/tendermint && git checkout "$(GIT_BRANCH)" && git pull - export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/tendermint/tendermint get_tools get_vendor_deps build + export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/tendermint/tendermint get_tools build cp $(GOPATH)/src/github.com/tendermint/tendermint/build/tendermint $(GOPATH)/bin @echo "*** Built tendermint" @@ -72,7 +72,7 @@ build-ethermint: git-branch gopath-setup @echo "*** Building ethermint" go get -d -u github.com/tendermint/ethermint/cmd/ethermint cd $(GOPATH)/src/github.com/tendermint/ethermint && git checkout "$(GIT_BRANCH)" && git pull - export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/tendermint/ethermint get_vendor_deps build + export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/tendermint/ethermint build cp $(GOPATH)/src/github.com/tendermint/ethermint/build/ethermint $(GOPATH)/bin @echo "*** Built ethermint" @@ -80,14 +80,14 @@ build-gaia: git-branch gopath-setup @echo "*** Building gaia" go get -d -u go github.com/cosmos/gaia || echo "Workaround for go downloads." cd $(GOPATH)/src/github.com/cosmos/gaia && git checkout "$(GIT_BRANCH)" && git pull - export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/cosmos/gaia get_vendor_deps install + export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/cosmos/gaia install @echo "*** Built gaia" build-basecoind: git-branch gopath-setup @echo "*** Building basecoind from cosmos-sdk" go get -d -u github.com/cosmos/cosmos-sdk/examples/basecoin/cmd/basecoind cd $(GOPATH)/src/github.com/cosmos/cosmos-sdk && git checkout "$(GIT_BRANCH)" && git pull - export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/cosmos/cosmos-sdk get_tools get_vendor_deps build + export PATH=$(GOPATH)/bin:$(PATH) && $(MAKE) -C $(GOPATH)/src/github.com/cosmos/cosmos-sdk get_tools build cp $(GOPATH)/src/github.com/cosmos/cosmos-sdk/build/basecoind $(GOPATH)/bin/basecoind @echo "*** Built basecoind from cosmos-sdk" diff --git a/tools/tm-bench/Dockerfile.dev b/tools/tm-bench/Dockerfile.dev index 469bb8150..1151965a2 100644 --- a/tools/tm-bench/Dockerfile.dev +++ b/tools/tm-bench/Dockerfile.dev @@ -9,4 +9,3 @@ RUN make get_tools COPY . /go/src/github.com/tendermint/tendermint/tools/tm-bench -RUN make get_vendor_deps diff --git a/tools/tm-bench/README.md b/tools/tm-bench/README.md index b4e8cec5a..d5ed1231f 100644 --- a/tools/tm-bench/README.md +++ b/tools/tm-bench/README.md @@ -100,6 +100,5 @@ Each of the connections is handled via two separate goroutines. ## Development ``` -make get_vendor_deps make test ``` diff --git a/tools/tm-monitor/Dockerfile.dev b/tools/tm-monitor/Dockerfile.dev index 5bfbbfd5a..e593bf89c 100644 --- a/tools/tm-monitor/Dockerfile.dev +++ b/tools/tm-monitor/Dockerfile.dev @@ -9,4 +9,3 @@ RUN make get_tools COPY . /go/src/github.com/tendermint/tools/tm-monitor -RUN make get_vendor_deps diff --git a/tools/tm-monitor/README.md b/tools/tm-monitor/README.md index 374a56b0a..2bd367b99 100644 --- a/tools/tm-monitor/README.md +++ b/tools/tm-monitor/README.md @@ -87,6 +87,5 @@ websocket. ``` make get_tools -make get_vendor_deps make test ``` From ab0835463f1f89dcadf83f9492e98d85583b0e71 Mon Sep 17 00:00:00 2001 From: Alexander Bezobchuk Date: Wed, 12 Jun 2019 14:03:45 +0200 Subject: [PATCH 061/211] abci: Refactor tagging events using list of lists (#3643) ## PR This PR introduces a fundamental breaking change to the structure of ABCI response and tx tags and the way they're processed. Namely, the SDK can support more complex and aggregated events for distribution and slashing. In addition, block responses can include duplicate keys in events. Implement new Event type. An event has a type and a list of KV pairs (ie. list-of-lists). Typical events may look like: "rewards": [{"amount": "5000uatom", "validator": "...", "recipient": "..."}] "sender": [{"address": "...", "balance": "100uatom"}] The events are indexed by {even.type}.{even.attribute[i].key}/.... In this case a client would subscribe or query for rewards.recipient='...' ABCI response types and related types now include Events []Event instead of Tags []cmn.KVPair. PubSub logic now publishes/matches against map[string][]string instead of map[string]string to support duplicate keys in response events (from #1385). A match is successful if the value is found in the slice of strings. closes: #1859 closes: #2905 ## Commits: * Implement Event ABCI type and updates responses to use events * Update messages_test.go * Update kvstore.go * Update event_bus.go * Update subscription.go * Update pubsub.go * Update kvstore.go * Update query logic to handle slice of strings in events * Update Empty#Matches and unit tests * Update pubsub logic * Update EventBus#Publish * Update kv tx indexer * Update godocs * Update ResultEvent to use slice of strings; update RPC * Update more tests * Update abci.md * Check for key in validateAndStringifyEvents * Fix KV indexer to skip empty keys * Fix linting errors * Update CHANGELOG_PENDING.md * Update docs/spec/abci/abci.md Co-Authored-By: Federico Kunze <31522760+fedekunze@users.noreply.github.com> * Update abci/types/types.proto Co-Authored-By: Ethan Buchman * Update docs/spec/abci/abci.md Co-Authored-By: Ethan Buchman * Update libs/pubsub/query/query.go Co-Authored-By: Ethan Buchman * Update match function to match if ANY value matches * Implement TestSubscribeDuplicateKeys * Update TestMatches to include multi-key test cases * Update events.go * Update Query interface godoc * Update match godoc * Add godoc for matchValue * DRY-up tx indexing * Return error from PublishWithEvents in EventBus#Publish * Update PublishEventNewBlockHeader to return an error * Fix build * Update events doc in ABCI * Update ABCI events godoc * Implement TestEventBusPublishEventTxDuplicateKeys * Update TestSubscribeDuplicateKeys to be table-driven * Remove mod file * Remove markdown from events godoc * Implement TestTxSearchDeprecatedIndexing test --- CHANGELOG_PENDING.md | 10 +- abci/example/kvstore/kvstore.go | 15 +- abci/types/messages_test.go | 19 +- abci/types/types.pb.go | 934 +++++++++++++++++++++----------- abci/types/types.proto | 13 +- abci/types/typespb_test.go | 124 +++++ blockchain/reactor_test.go | 2 +- docs/spec/abci/abci.md | 54 +- libs/pubsub/example_test.go | 2 +- libs/pubsub/pubsub.go | 37 +- libs/pubsub/pubsub_test.go | 65 ++- libs/pubsub/query/empty.go | 2 +- libs/pubsub/query/empty_test.go | 8 +- libs/pubsub/query/query.go | 72 ++- libs/pubsub/query/query_test.go | 58 +- libs/pubsub/subscription.go | 14 +- rpc/client/localclient.go | 2 +- rpc/core/events.go | 89 ++- rpc/core/types/responses.go | 6 +- state/execution_test.go | 3 +- state/state_test.go | 16 +- state/txindex/kv/kv.go | 59 +- state/txindex/kv/kv_test.go | 144 ++++- types/event_bus.go | 79 ++- types/event_bus_test.go | 137 ++++- 25 files changed, 1409 insertions(+), 555 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 45ed56919..c23dd76b9 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -10,14 +10,18 @@ - read more on Modules here: https://github.com/golang/go/wiki/Modules * CLI/RPC/Config -- [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` -> + * [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` -> `results.deliver_tx`). See docs for details. * Apps + * [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, + and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` + contains a `type` and a list of `attributes` (list of key-value pairs) allowing + for inclusion of multiple distinct events in each response. * Go API -- [libs/db] Removed deprecated `LevelDBBackend` const - * If you have `db_backend` set to `leveldb` in your config file, please + * [libs/db] Removed deprecated `LevelDBBackend` const + * If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index 955baefb4..0c28813f2 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -84,14 +84,21 @@ func (app *KVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { } else { key, value = tx, tx } + app.state.db.Set(prefixKey(key), value) app.state.Size += 1 - tags := []cmn.KVPair{ - {Key: []byte("app.creator"), Value: []byte("Cosmoshi Netowoko")}, - {Key: []byte("app.key"), Value: key}, + events := []types.Event{ + { + Type: "app", + Attributes: []cmn.KVPair{ + {Key: []byte("creator"), Value: []byte("Cosmoshi Netowoko")}, + {Key: []byte("key"), Value: key}, + }, + }, } - return types.ResponseDeliverTx{Code: code.CodeTypeOK, Tags: tags} + + return types.ResponseDeliverTx{Code: code.CodeTypeOK, Events: events} } func (app *KVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx { diff --git a/abci/types/messages_test.go b/abci/types/messages_test.go index 762111b61..904b16410 100644 --- a/abci/types/messages_test.go +++ b/abci/types/messages_test.go @@ -8,6 +8,7 @@ import ( "github.com/gogo/protobuf/proto" "github.com/stretchr/testify/assert" + cmn "github.com/tendermint/tendermint/libs/common" ) @@ -21,8 +22,13 @@ func TestMarshalJSON(t *testing.T) { Code: 1, Data: []byte("hello"), GasWanted: 43, - Tags: []cmn.KVPair{ - {Key: []byte("pho"), Value: []byte("bo")}, + Events: []Event{ + { + Type: "testEvent", + Attributes: []cmn.KVPair{ + {Key: []byte("pho"), Value: []byte("bo")}, + }, + }, }, } b, err = json.Marshal(&r1) @@ -82,8 +88,13 @@ func TestWriteReadMessage2(t *testing.T) { Data: []byte(phrase), Log: phrase, GasWanted: 10, - Tags: []cmn.KVPair{ - {Key: []byte("abc"), Value: []byte("def")}, + Events: []Event{ + { + Type: "testEvent", + Attributes: []cmn.KVPair{ + {Key: []byte("abc"), Value: []byte("def")}, + }, + }, }, }, // TODO: add the rest diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index 7bf50f330..a7455b523 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -61,7 +61,7 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{0} + return fileDescriptor_types_62f0c59aeb977f78, []int{0} } func (m *Request) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -483,7 +483,7 @@ func (m *RequestEcho) Reset() { *m = RequestEcho{} } func (m *RequestEcho) String() string { return proto.CompactTextString(m) } func (*RequestEcho) ProtoMessage() {} func (*RequestEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{1} + return fileDescriptor_types_62f0c59aeb977f78, []int{1} } func (m *RequestEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -529,7 +529,7 @@ func (m *RequestFlush) Reset() { *m = RequestFlush{} } func (m *RequestFlush) String() string { return proto.CompactTextString(m) } func (*RequestFlush) ProtoMessage() {} func (*RequestFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{2} + return fileDescriptor_types_62f0c59aeb977f78, []int{2} } func (m *RequestFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -571,7 +571,7 @@ func (m *RequestInfo) Reset() { *m = RequestInfo{} } func (m *RequestInfo) String() string { return proto.CompactTextString(m) } func (*RequestInfo) ProtoMessage() {} func (*RequestInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{3} + return fileDescriptor_types_62f0c59aeb977f78, []int{3} } func (m *RequestInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -634,7 +634,7 @@ func (m *RequestSetOption) Reset() { *m = RequestSetOption{} } func (m *RequestSetOption) String() string { return proto.CompactTextString(m) } func (*RequestSetOption) ProtoMessage() {} func (*RequestSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{4} + return fileDescriptor_types_62f0c59aeb977f78, []int{4} } func (m *RequestSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -692,7 +692,7 @@ func (m *RequestInitChain) Reset() { *m = RequestInitChain{} } func (m *RequestInitChain) String() string { return proto.CompactTextString(m) } func (*RequestInitChain) ProtoMessage() {} func (*RequestInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{5} + return fileDescriptor_types_62f0c59aeb977f78, []int{5} } func (m *RequestInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -770,7 +770,7 @@ func (m *RequestQuery) Reset() { *m = RequestQuery{} } func (m *RequestQuery) String() string { return proto.CompactTextString(m) } func (*RequestQuery) ProtoMessage() {} func (*RequestQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{6} + return fileDescriptor_types_62f0c59aeb977f78, []int{6} } func (m *RequestQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -841,7 +841,7 @@ func (m *RequestBeginBlock) Reset() { *m = RequestBeginBlock{} } func (m *RequestBeginBlock) String() string { return proto.CompactTextString(m) } func (*RequestBeginBlock) ProtoMessage() {} func (*RequestBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{7} + return fileDescriptor_types_62f0c59aeb977f78, []int{7} } func (m *RequestBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -909,7 +909,7 @@ func (m *RequestCheckTx) Reset() { *m = RequestCheckTx{} } func (m *RequestCheckTx) String() string { return proto.CompactTextString(m) } func (*RequestCheckTx) ProtoMessage() {} func (*RequestCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{8} + return fileDescriptor_types_62f0c59aeb977f78, []int{8} } func (m *RequestCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -956,7 +956,7 @@ func (m *RequestDeliverTx) Reset() { *m = RequestDeliverTx{} } func (m *RequestDeliverTx) String() string { return proto.CompactTextString(m) } func (*RequestDeliverTx) ProtoMessage() {} func (*RequestDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{9} + return fileDescriptor_types_62f0c59aeb977f78, []int{9} } func (m *RequestDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1003,7 +1003,7 @@ func (m *RequestEndBlock) Reset() { *m = RequestEndBlock{} } func (m *RequestEndBlock) String() string { return proto.CompactTextString(m) } func (*RequestEndBlock) ProtoMessage() {} func (*RequestEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{10} + return fileDescriptor_types_62f0c59aeb977f78, []int{10} } func (m *RequestEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1049,7 +1049,7 @@ func (m *RequestCommit) Reset() { *m = RequestCommit{} } func (m *RequestCommit) String() string { return proto.CompactTextString(m) } func (*RequestCommit) ProtoMessage() {} func (*RequestCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{11} + return fileDescriptor_types_62f0c59aeb977f78, []int{11} } func (m *RequestCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1102,7 +1102,7 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{12} + return fileDescriptor_types_62f0c59aeb977f78, []int{12} } func (m *Response) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1555,7 +1555,7 @@ func (m *ResponseException) Reset() { *m = ResponseException{} } func (m *ResponseException) String() string { return proto.CompactTextString(m) } func (*ResponseException) ProtoMessage() {} func (*ResponseException) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{13} + return fileDescriptor_types_62f0c59aeb977f78, []int{13} } func (m *ResponseException) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1602,7 +1602,7 @@ func (m *ResponseEcho) Reset() { *m = ResponseEcho{} } func (m *ResponseEcho) String() string { return proto.CompactTextString(m) } func (*ResponseEcho) ProtoMessage() {} func (*ResponseEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{14} + return fileDescriptor_types_62f0c59aeb977f78, []int{14} } func (m *ResponseEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1648,7 +1648,7 @@ func (m *ResponseFlush) Reset() { *m = ResponseFlush{} } func (m *ResponseFlush) String() string { return proto.CompactTextString(m) } func (*ResponseFlush) ProtoMessage() {} func (*ResponseFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{15} + return fileDescriptor_types_62f0c59aeb977f78, []int{15} } func (m *ResponseFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1692,7 +1692,7 @@ func (m *ResponseInfo) Reset() { *m = ResponseInfo{} } func (m *ResponseInfo) String() string { return proto.CompactTextString(m) } func (*ResponseInfo) ProtoMessage() {} func (*ResponseInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{16} + return fileDescriptor_types_62f0c59aeb977f78, []int{16} } func (m *ResponseInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1771,7 +1771,7 @@ func (m *ResponseSetOption) Reset() { *m = ResponseSetOption{} } func (m *ResponseSetOption) String() string { return proto.CompactTextString(m) } func (*ResponseSetOption) ProtoMessage() {} func (*ResponseSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{17} + return fileDescriptor_types_62f0c59aeb977f78, []int{17} } func (m *ResponseSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1833,7 +1833,7 @@ func (m *ResponseInitChain) Reset() { *m = ResponseInitChain{} } func (m *ResponseInitChain) String() string { return proto.CompactTextString(m) } func (*ResponseInitChain) ProtoMessage() {} func (*ResponseInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{18} + return fileDescriptor_types_62f0c59aeb977f78, []int{18} } func (m *ResponseInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1896,7 +1896,7 @@ func (m *ResponseQuery) Reset() { *m = ResponseQuery{} } func (m *ResponseQuery) String() string { return proto.CompactTextString(m) } func (*ResponseQuery) ProtoMessage() {} func (*ResponseQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{19} + return fileDescriptor_types_62f0c59aeb977f78, []int{19} } func (m *ResponseQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1989,17 +1989,17 @@ func (m *ResponseQuery) GetCodespace() string { } type ResponseBeginBlock struct { - Tags []common.KVPair `protobuf:"bytes,1,rep,name=tags" json:"tags,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Events []Event `protobuf:"bytes,1,rep,name=events" json:"events,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *ResponseBeginBlock) Reset() { *m = ResponseBeginBlock{} } func (m *ResponseBeginBlock) String() string { return proto.CompactTextString(m) } func (*ResponseBeginBlock) ProtoMessage() {} func (*ResponseBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{20} + return fileDescriptor_types_62f0c59aeb977f78, []int{20} } func (m *ResponseBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2028,32 +2028,32 @@ func (m *ResponseBeginBlock) XXX_DiscardUnknown() { var xxx_messageInfo_ResponseBeginBlock proto.InternalMessageInfo -func (m *ResponseBeginBlock) GetTags() []common.KVPair { +func (m *ResponseBeginBlock) GetEvents() []Event { if m != nil { - return m.Tags + return m.Events } return nil } type ResponseCheckTx struct { - Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` - Log string `protobuf:"bytes,3,opt,name=log,proto3" json:"log,omitempty"` - Info string `protobuf:"bytes,4,opt,name=info,proto3" json:"info,omitempty"` - GasWanted int64 `protobuf:"varint,5,opt,name=gas_wanted,json=gasWanted,proto3" json:"gas_wanted,omitempty"` - GasUsed int64 `protobuf:"varint,6,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` - Tags []common.KVPair `protobuf:"bytes,7,rep,name=tags" json:"tags,omitempty"` - Codespace string `protobuf:"bytes,8,opt,name=codespace,proto3" json:"codespace,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Log string `protobuf:"bytes,3,opt,name=log,proto3" json:"log,omitempty"` + Info string `protobuf:"bytes,4,opt,name=info,proto3" json:"info,omitempty"` + GasWanted int64 `protobuf:"varint,5,opt,name=gas_wanted,json=gasWanted,proto3" json:"gas_wanted,omitempty"` + GasUsed int64 `protobuf:"varint,6,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` + Events []Event `protobuf:"bytes,7,rep,name=events" json:"events,omitempty"` + Codespace string `protobuf:"bytes,8,opt,name=codespace,proto3" json:"codespace,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *ResponseCheckTx) Reset() { *m = ResponseCheckTx{} } func (m *ResponseCheckTx) String() string { return proto.CompactTextString(m) } func (*ResponseCheckTx) ProtoMessage() {} func (*ResponseCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{21} + return fileDescriptor_types_62f0c59aeb977f78, []int{21} } func (m *ResponseCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2124,9 +2124,9 @@ func (m *ResponseCheckTx) GetGasUsed() int64 { return 0 } -func (m *ResponseCheckTx) GetTags() []common.KVPair { +func (m *ResponseCheckTx) GetEvents() []Event { if m != nil { - return m.Tags + return m.Events } return nil } @@ -2139,24 +2139,24 @@ func (m *ResponseCheckTx) GetCodespace() string { } type ResponseDeliverTx struct { - Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` - Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` - Log string `protobuf:"bytes,3,opt,name=log,proto3" json:"log,omitempty"` - Info string `protobuf:"bytes,4,opt,name=info,proto3" json:"info,omitempty"` - GasWanted int64 `protobuf:"varint,5,opt,name=gas_wanted,json=gasWanted,proto3" json:"gas_wanted,omitempty"` - GasUsed int64 `protobuf:"varint,6,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` - Tags []common.KVPair `protobuf:"bytes,7,rep,name=tags" json:"tags,omitempty"` - Codespace string `protobuf:"bytes,8,opt,name=codespace,proto3" json:"codespace,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Log string `protobuf:"bytes,3,opt,name=log,proto3" json:"log,omitempty"` + Info string `protobuf:"bytes,4,opt,name=info,proto3" json:"info,omitempty"` + GasWanted int64 `protobuf:"varint,5,opt,name=gas_wanted,json=gasWanted,proto3" json:"gas_wanted,omitempty"` + GasUsed int64 `protobuf:"varint,6,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` + Events []Event `protobuf:"bytes,7,rep,name=events" json:"events,omitempty"` + Codespace string `protobuf:"bytes,8,opt,name=codespace,proto3" json:"codespace,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *ResponseDeliverTx) Reset() { *m = ResponseDeliverTx{} } func (m *ResponseDeliverTx) String() string { return proto.CompactTextString(m) } func (*ResponseDeliverTx) ProtoMessage() {} func (*ResponseDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{22} + return fileDescriptor_types_62f0c59aeb977f78, []int{22} } func (m *ResponseDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2227,9 +2227,9 @@ func (m *ResponseDeliverTx) GetGasUsed() int64 { return 0 } -func (m *ResponseDeliverTx) GetTags() []common.KVPair { +func (m *ResponseDeliverTx) GetEvents() []Event { if m != nil { - return m.Tags + return m.Events } return nil } @@ -2244,7 +2244,7 @@ func (m *ResponseDeliverTx) GetCodespace() string { type ResponseEndBlock struct { ValidatorUpdates []ValidatorUpdate `protobuf:"bytes,1,rep,name=validator_updates,json=validatorUpdates" json:"validator_updates"` ConsensusParamUpdates *ConsensusParams `protobuf:"bytes,2,opt,name=consensus_param_updates,json=consensusParamUpdates" json:"consensus_param_updates,omitempty"` - Tags []common.KVPair `protobuf:"bytes,3,rep,name=tags" json:"tags,omitempty"` + Events []Event `protobuf:"bytes,3,rep,name=events" json:"events,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` @@ -2254,7 +2254,7 @@ func (m *ResponseEndBlock) Reset() { *m = ResponseEndBlock{} } func (m *ResponseEndBlock) String() string { return proto.CompactTextString(m) } func (*ResponseEndBlock) ProtoMessage() {} func (*ResponseEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{23} + return fileDescriptor_types_62f0c59aeb977f78, []int{23} } func (m *ResponseEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2297,9 +2297,9 @@ func (m *ResponseEndBlock) GetConsensusParamUpdates() *ConsensusParams { return nil } -func (m *ResponseEndBlock) GetTags() []common.KVPair { +func (m *ResponseEndBlock) GetEvents() []Event { if m != nil { - return m.Tags + return m.Events } return nil } @@ -2316,7 +2316,7 @@ func (m *ResponseCommit) Reset() { *m = ResponseCommit{} } func (m *ResponseCommit) String() string { return proto.CompactTextString(m) } func (*ResponseCommit) ProtoMessage() {} func (*ResponseCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{24} + return fileDescriptor_types_62f0c59aeb977f78, []int{24} } func (m *ResponseCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2367,7 +2367,7 @@ func (m *ConsensusParams) Reset() { *m = ConsensusParams{} } func (m *ConsensusParams) String() string { return proto.CompactTextString(m) } func (*ConsensusParams) ProtoMessage() {} func (*ConsensusParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{25} + return fileDescriptor_types_62f0c59aeb977f78, []int{25} } func (m *ConsensusParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2417,7 +2417,7 @@ func (m *ConsensusParams) GetValidator() *ValidatorParams { return nil } -// BlockParams contains limits on the block size. +// BlockParams contains limits on the block size and timestamp. type BlockParams struct { // Note: must be greater than 0 MaxBytes int64 `protobuf:"varint,1,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"` @@ -2432,7 +2432,7 @@ func (m *BlockParams) Reset() { *m = BlockParams{} } func (m *BlockParams) String() string { return proto.CompactTextString(m) } func (*BlockParams) ProtoMessage() {} func (*BlockParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{26} + return fileDescriptor_types_62f0c59aeb977f78, []int{26} } func (m *BlockParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2488,7 +2488,7 @@ func (m *EvidenceParams) Reset() { *m = EvidenceParams{} } func (m *EvidenceParams) String() string { return proto.CompactTextString(m) } func (*EvidenceParams) ProtoMessage() {} func (*EvidenceParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{27} + return fileDescriptor_types_62f0c59aeb977f78, []int{27} } func (m *EvidenceParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2536,7 +2536,7 @@ func (m *ValidatorParams) Reset() { *m = ValidatorParams{} } func (m *ValidatorParams) String() string { return proto.CompactTextString(m) } func (*ValidatorParams) ProtoMessage() {} func (*ValidatorParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{28} + return fileDescriptor_types_62f0c59aeb977f78, []int{28} } func (m *ValidatorParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2584,7 +2584,7 @@ func (m *LastCommitInfo) Reset() { *m = LastCommitInfo{} } func (m *LastCommitInfo) String() string { return proto.CompactTextString(m) } func (*LastCommitInfo) ProtoMessage() {} func (*LastCommitInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{29} + return fileDescriptor_types_62f0c59aeb977f78, []int{29} } func (m *LastCommitInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2627,6 +2627,61 @@ func (m *LastCommitInfo) GetVotes() []VoteInfo { return nil } +type Event struct { + Type string `protobuf:"bytes,1,opt,name=type,proto3" json:"type,omitempty"` + Attributes []common.KVPair `protobuf:"bytes,2,rep,name=attributes" json:"attributes,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *Event) Reset() { *m = Event{} } +func (m *Event) String() string { return proto.CompactTextString(m) } +func (*Event) ProtoMessage() {} +func (*Event) Descriptor() ([]byte, []int) { + return fileDescriptor_types_62f0c59aeb977f78, []int{30} +} +func (m *Event) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *Event) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_Event.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (dst *Event) XXX_Merge(src proto.Message) { + xxx_messageInfo_Event.Merge(dst, src) +} +func (m *Event) XXX_Size() int { + return m.Size() +} +func (m *Event) XXX_DiscardUnknown() { + xxx_messageInfo_Event.DiscardUnknown(m) +} + +var xxx_messageInfo_Event proto.InternalMessageInfo + +func (m *Event) GetType() string { + if m != nil { + return m.Type + } + return "" +} + +func (m *Event) GetAttributes() []common.KVPair { + if m != nil { + return m.Attributes + } + return nil +} + type Header struct { // basic block info Version Version `protobuf:"bytes,1,opt,name=version" json:"version"` @@ -2658,7 +2713,7 @@ func (m *Header) Reset() { *m = Header{} } func (m *Header) String() string { return proto.CompactTextString(m) } func (*Header) ProtoMessage() {} func (*Header) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{30} + return fileDescriptor_types_62f0c59aeb977f78, []int{31} } func (m *Header) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2811,7 +2866,7 @@ func (m *Version) Reset() { *m = Version{} } func (m *Version) String() string { return proto.CompactTextString(m) } func (*Version) ProtoMessage() {} func (*Version) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{31} + return fileDescriptor_types_62f0c59aeb977f78, []int{32} } func (m *Version) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2866,7 +2921,7 @@ func (m *BlockID) Reset() { *m = BlockID{} } func (m *BlockID) String() string { return proto.CompactTextString(m) } func (*BlockID) ProtoMessage() {} func (*BlockID) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{32} + return fileDescriptor_types_62f0c59aeb977f78, []int{33} } func (m *BlockID) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2921,7 +2976,7 @@ func (m *PartSetHeader) Reset() { *m = PartSetHeader{} } func (m *PartSetHeader) String() string { return proto.CompactTextString(m) } func (*PartSetHeader) ProtoMessage() {} func (*PartSetHeader) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{33} + return fileDescriptor_types_62f0c59aeb977f78, []int{34} } func (m *PartSetHeader) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2978,7 +3033,7 @@ func (m *Validator) Reset() { *m = Validator{} } func (m *Validator) String() string { return proto.CompactTextString(m) } func (*Validator) ProtoMessage() {} func (*Validator) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{34} + return fileDescriptor_types_62f0c59aeb977f78, []int{35} } func (m *Validator) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3034,7 +3089,7 @@ func (m *ValidatorUpdate) Reset() { *m = ValidatorUpdate{} } func (m *ValidatorUpdate) String() string { return proto.CompactTextString(m) } func (*ValidatorUpdate) ProtoMessage() {} func (*ValidatorUpdate) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{35} + return fileDescriptor_types_62f0c59aeb977f78, []int{36} } func (m *ValidatorUpdate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3090,7 +3145,7 @@ func (m *VoteInfo) Reset() { *m = VoteInfo{} } func (m *VoteInfo) String() string { return proto.CompactTextString(m) } func (*VoteInfo) ProtoMessage() {} func (*VoteInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{36} + return fileDescriptor_types_62f0c59aeb977f78, []int{37} } func (m *VoteInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3145,7 +3200,7 @@ func (m *PubKey) Reset() { *m = PubKey{} } func (m *PubKey) String() string { return proto.CompactTextString(m) } func (*PubKey) ProtoMessage() {} func (*PubKey) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{37} + return fileDescriptor_types_62f0c59aeb977f78, []int{38} } func (m *PubKey) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3203,7 +3258,7 @@ func (m *Evidence) Reset() { *m = Evidence{} } func (m *Evidence) String() string { return proto.CompactTextString(m) } func (*Evidence) ProtoMessage() {} func (*Evidence) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7e896a7c04915591, []int{38} + return fileDescriptor_types_62f0c59aeb977f78, []int{39} } func (m *Evidence) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3328,6 +3383,8 @@ func init() { golang_proto.RegisterType((*ValidatorParams)(nil), "types.ValidatorParams") proto.RegisterType((*LastCommitInfo)(nil), "types.LastCommitInfo") golang_proto.RegisterType((*LastCommitInfo)(nil), "types.LastCommitInfo") + proto.RegisterType((*Event)(nil), "types.Event") + golang_proto.RegisterType((*Event)(nil), "types.Event") proto.RegisterType((*Header)(nil), "types.Header") golang_proto.RegisterType((*Header)(nil), "types.Header") proto.RegisterType((*Version)(nil), "types.Version") @@ -4560,11 +4617,11 @@ func (this *ResponseBeginBlock) Equal(that interface{}) bool { } else if this == nil { return false } - if len(this.Tags) != len(that1.Tags) { + if len(this.Events) != len(that1.Events) { return false } - for i := range this.Tags { - if !this.Tags[i].Equal(&that1.Tags[i]) { + for i := range this.Events { + if !this.Events[i].Equal(&that1.Events[i]) { return false } } @@ -4610,11 +4667,11 @@ func (this *ResponseCheckTx) Equal(that interface{}) bool { if this.GasUsed != that1.GasUsed { return false } - if len(this.Tags) != len(that1.Tags) { + if len(this.Events) != len(that1.Events) { return false } - for i := range this.Tags { - if !this.Tags[i].Equal(&that1.Tags[i]) { + for i := range this.Events { + if !this.Events[i].Equal(&that1.Events[i]) { return false } } @@ -4663,11 +4720,11 @@ func (this *ResponseDeliverTx) Equal(that interface{}) bool { if this.GasUsed != that1.GasUsed { return false } - if len(this.Tags) != len(that1.Tags) { + if len(this.Events) != len(that1.Events) { return false } - for i := range this.Tags { - if !this.Tags[i].Equal(&that1.Tags[i]) { + for i := range this.Events { + if !this.Events[i].Equal(&that1.Events[i]) { return false } } @@ -4709,11 +4766,11 @@ func (this *ResponseEndBlock) Equal(that interface{}) bool { if !this.ConsensusParamUpdates.Equal(that1.ConsensusParamUpdates) { return false } - if len(this.Tags) != len(that1.Tags) { + if len(this.Events) != len(that1.Events) { return false } - for i := range this.Tags { - if !this.Tags[i].Equal(&that1.Tags[i]) { + for i := range this.Events { + if !this.Events[i].Equal(&that1.Events[i]) { return false } } @@ -4906,6 +4963,41 @@ func (this *LastCommitInfo) Equal(that interface{}) bool { } return true } +func (this *Event) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*Event) + if !ok { + that2, ok := that.(Event) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Type != that1.Type { + return false + } + if len(this.Attributes) != len(that1.Attributes) { + return false + } + for i := range this.Attributes { + if !this.Attributes[i].Equal(&that1.Attributes[i]) { + return false + } + } + if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { + return false + } + return true +} func (this *Header) Equal(that interface{}) bool { if that == nil { return this == nil @@ -6691,8 +6783,8 @@ func (m *ResponseBeginBlock) MarshalTo(dAtA []byte) (int, error) { _ = i var l int _ = l - if len(m.Tags) > 0 { - for _, msg := range m.Tags { + if len(m.Events) > 0 { + for _, msg := range m.Events { dAtA[i] = 0xa i++ i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) @@ -6757,8 +6849,8 @@ func (m *ResponseCheckTx) MarshalTo(dAtA []byte) (int, error) { i++ i = encodeVarintTypes(dAtA, i, uint64(m.GasUsed)) } - if len(m.Tags) > 0 { - for _, msg := range m.Tags { + if len(m.Events) > 0 { + for _, msg := range m.Events { dAtA[i] = 0x3a i++ i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) @@ -6829,8 +6921,8 @@ func (m *ResponseDeliverTx) MarshalTo(dAtA []byte) (int, error) { i++ i = encodeVarintTypes(dAtA, i, uint64(m.GasUsed)) } - if len(m.Tags) > 0 { - for _, msg := range m.Tags { + if len(m.Events) > 0 { + for _, msg := range m.Events { dAtA[i] = 0x3a i++ i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) @@ -6890,8 +6982,8 @@ func (m *ResponseEndBlock) MarshalTo(dAtA []byte) (int, error) { } i += n32 } - if len(m.Tags) > 0 { - for _, msg := range m.Tags { + if len(m.Events) > 0 { + for _, msg := range m.Events { dAtA[i] = 0x1a i++ i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) @@ -7117,6 +7209,45 @@ func (m *LastCommitInfo) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *Event) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *Event) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Type) > 0 { + dAtA[i] = 0xa + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Type))) + i += copy(dAtA[i:], m.Type) + } + if len(m.Attributes) > 0 { + for _, msg := range m.Attributes { + dAtA[i] = 0x12 + i++ + i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.XXX_unrecognized != nil { + i += copy(dAtA[i:], m.XXX_unrecognized) + } + return i, nil +} + func (m *Header) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -7988,10 +8119,10 @@ func NewPopulatedResponseBeginBlock(r randyTypes, easy bool) *ResponseBeginBlock this := &ResponseBeginBlock{} if r.Intn(10) != 0 { v18 := r.Intn(5) - this.Tags = make([]common.KVPair, v18) + this.Events = make([]Event, v18) for i := 0; i < v18; i++ { - v19 := common.NewPopulatedKVPair(r, easy) - this.Tags[i] = *v19 + v19 := NewPopulatedEvent(r, easy) + this.Events[i] = *v19 } } if !easy && r.Intn(10) != 0 { @@ -8020,10 +8151,10 @@ func NewPopulatedResponseCheckTx(r randyTypes, easy bool) *ResponseCheckTx { } if r.Intn(10) != 0 { v21 := r.Intn(5) - this.Tags = make([]common.KVPair, v21) + this.Events = make([]Event, v21) for i := 0; i < v21; i++ { - v22 := common.NewPopulatedKVPair(r, easy) - this.Tags[i] = *v22 + v22 := NewPopulatedEvent(r, easy) + this.Events[i] = *v22 } } this.Codespace = string(randStringTypes(r)) @@ -8053,10 +8184,10 @@ func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { } if r.Intn(10) != 0 { v24 := r.Intn(5) - this.Tags = make([]common.KVPair, v24) + this.Events = make([]Event, v24) for i := 0; i < v24; i++ { - v25 := common.NewPopulatedKVPair(r, easy) - this.Tags[i] = *v25 + v25 := NewPopulatedEvent(r, easy) + this.Events[i] = *v25 } } this.Codespace = string(randStringTypes(r)) @@ -8081,10 +8212,10 @@ func NewPopulatedResponseEndBlock(r randyTypes, easy bool) *ResponseEndBlock { } if r.Intn(10) != 0 { v28 := r.Intn(5) - this.Tags = make([]common.KVPair, v28) + this.Events = make([]Event, v28) for i := 0; i < v28; i++ { - v29 := common.NewPopulatedKVPair(r, easy) - this.Tags[i] = *v29 + v29 := NewPopulatedEvent(r, easy) + this.Events[i] = *v29 } } if !easy && r.Intn(10) != 0 { @@ -8184,17 +8315,34 @@ func NewPopulatedLastCommitInfo(r randyTypes, easy bool) *LastCommitInfo { return this } +func NewPopulatedEvent(r randyTypes, easy bool) *Event { + this := &Event{} + this.Type = string(randStringTypes(r)) + if r.Intn(10) != 0 { + v34 := r.Intn(5) + this.Attributes = make([]common.KVPair, v34) + for i := 0; i < v34; i++ { + v35 := common.NewPopulatedKVPair(r, easy) + this.Attributes[i] = *v35 + } + } + if !easy && r.Intn(10) != 0 { + this.XXX_unrecognized = randUnrecognizedTypes(r, 3) + } + return this +} + func NewPopulatedHeader(r randyTypes, easy bool) *Header { this := &Header{} - v34 := NewPopulatedVersion(r, easy) - this.Version = *v34 + v36 := NewPopulatedVersion(r, easy) + this.Version = *v36 this.ChainID = string(randStringTypes(r)) this.Height = int64(r.Int63()) if r.Intn(2) == 0 { this.Height *= -1 } - v35 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v35 + v37 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v37 this.NumTxs = int64(r.Int63()) if r.Intn(2) == 0 { this.NumTxs *= -1 @@ -8203,51 +8351,51 @@ func NewPopulatedHeader(r randyTypes, easy bool) *Header { if r.Intn(2) == 0 { this.TotalTxs *= -1 } - v36 := NewPopulatedBlockID(r, easy) - this.LastBlockId = *v36 - v37 := r.Intn(100) - this.LastCommitHash = make([]byte, v37) - for i := 0; i < v37; i++ { - this.LastCommitHash[i] = byte(r.Intn(256)) - } - v38 := r.Intn(100) - this.DataHash = make([]byte, v38) - for i := 0; i < v38; i++ { - this.DataHash[i] = byte(r.Intn(256)) - } + v38 := NewPopulatedBlockID(r, easy) + this.LastBlockId = *v38 v39 := r.Intn(100) - this.ValidatorsHash = make([]byte, v39) + this.LastCommitHash = make([]byte, v39) for i := 0; i < v39; i++ { - this.ValidatorsHash[i] = byte(r.Intn(256)) + this.LastCommitHash[i] = byte(r.Intn(256)) } v40 := r.Intn(100) - this.NextValidatorsHash = make([]byte, v40) + this.DataHash = make([]byte, v40) for i := 0; i < v40; i++ { - this.NextValidatorsHash[i] = byte(r.Intn(256)) + this.DataHash[i] = byte(r.Intn(256)) } v41 := r.Intn(100) - this.ConsensusHash = make([]byte, v41) + this.ValidatorsHash = make([]byte, v41) for i := 0; i < v41; i++ { - this.ConsensusHash[i] = byte(r.Intn(256)) + this.ValidatorsHash[i] = byte(r.Intn(256)) } v42 := r.Intn(100) - this.AppHash = make([]byte, v42) + this.NextValidatorsHash = make([]byte, v42) for i := 0; i < v42; i++ { - this.AppHash[i] = byte(r.Intn(256)) + this.NextValidatorsHash[i] = byte(r.Intn(256)) } v43 := r.Intn(100) - this.LastResultsHash = make([]byte, v43) + this.ConsensusHash = make([]byte, v43) for i := 0; i < v43; i++ { - this.LastResultsHash[i] = byte(r.Intn(256)) + this.ConsensusHash[i] = byte(r.Intn(256)) } v44 := r.Intn(100) - this.EvidenceHash = make([]byte, v44) + this.AppHash = make([]byte, v44) for i := 0; i < v44; i++ { - this.EvidenceHash[i] = byte(r.Intn(256)) + this.AppHash[i] = byte(r.Intn(256)) } v45 := r.Intn(100) - this.ProposerAddress = make([]byte, v45) + this.LastResultsHash = make([]byte, v45) for i := 0; i < v45; i++ { + this.LastResultsHash[i] = byte(r.Intn(256)) + } + v46 := r.Intn(100) + this.EvidenceHash = make([]byte, v46) + for i := 0; i < v46; i++ { + this.EvidenceHash[i] = byte(r.Intn(256)) + } + v47 := r.Intn(100) + this.ProposerAddress = make([]byte, v47) + for i := 0; i < v47; i++ { this.ProposerAddress[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8268,13 +8416,13 @@ func NewPopulatedVersion(r randyTypes, easy bool) *Version { func NewPopulatedBlockID(r randyTypes, easy bool) *BlockID { this := &BlockID{} - v46 := r.Intn(100) - this.Hash = make([]byte, v46) - for i := 0; i < v46; i++ { + v48 := r.Intn(100) + this.Hash = make([]byte, v48) + for i := 0; i < v48; i++ { this.Hash[i] = byte(r.Intn(256)) } - v47 := NewPopulatedPartSetHeader(r, easy) - this.PartsHeader = *v47 + v49 := NewPopulatedPartSetHeader(r, easy) + this.PartsHeader = *v49 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedTypes(r, 3) } @@ -8287,9 +8435,9 @@ func NewPopulatedPartSetHeader(r randyTypes, easy bool) *PartSetHeader { if r.Intn(2) == 0 { this.Total *= -1 } - v48 := r.Intn(100) - this.Hash = make([]byte, v48) - for i := 0; i < v48; i++ { + v50 := r.Intn(100) + this.Hash = make([]byte, v50) + for i := 0; i < v50; i++ { this.Hash[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8300,9 +8448,9 @@ func NewPopulatedPartSetHeader(r randyTypes, easy bool) *PartSetHeader { func NewPopulatedValidator(r randyTypes, easy bool) *Validator { this := &Validator{} - v49 := r.Intn(100) - this.Address = make([]byte, v49) - for i := 0; i < v49; i++ { + v51 := r.Intn(100) + this.Address = make([]byte, v51) + for i := 0; i < v51; i++ { this.Address[i] = byte(r.Intn(256)) } this.Power = int64(r.Int63()) @@ -8317,8 +8465,8 @@ func NewPopulatedValidator(r randyTypes, easy bool) *Validator { func NewPopulatedValidatorUpdate(r randyTypes, easy bool) *ValidatorUpdate { this := &ValidatorUpdate{} - v50 := NewPopulatedPubKey(r, easy) - this.PubKey = *v50 + v52 := NewPopulatedPubKey(r, easy) + this.PubKey = *v52 this.Power = int64(r.Int63()) if r.Intn(2) == 0 { this.Power *= -1 @@ -8331,8 +8479,8 @@ func NewPopulatedValidatorUpdate(r randyTypes, easy bool) *ValidatorUpdate { func NewPopulatedVoteInfo(r randyTypes, easy bool) *VoteInfo { this := &VoteInfo{} - v51 := NewPopulatedValidator(r, easy) - this.Validator = *v51 + v53 := NewPopulatedValidator(r, easy) + this.Validator = *v53 this.SignedLastBlock = bool(bool(r.Intn(2) == 0)) if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedTypes(r, 3) @@ -8343,9 +8491,9 @@ func NewPopulatedVoteInfo(r randyTypes, easy bool) *VoteInfo { func NewPopulatedPubKey(r randyTypes, easy bool) *PubKey { this := &PubKey{} this.Type = string(randStringTypes(r)) - v52 := r.Intn(100) - this.Data = make([]byte, v52) - for i := 0; i < v52; i++ { + v54 := r.Intn(100) + this.Data = make([]byte, v54) + for i := 0; i < v54; i++ { this.Data[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8357,14 +8505,14 @@ func NewPopulatedPubKey(r randyTypes, easy bool) *PubKey { func NewPopulatedEvidence(r randyTypes, easy bool) *Evidence { this := &Evidence{} this.Type = string(randStringTypes(r)) - v53 := NewPopulatedValidator(r, easy) - this.Validator = *v53 + v55 := NewPopulatedValidator(r, easy) + this.Validator = *v55 this.Height = int64(r.Int63()) if r.Intn(2) == 0 { this.Height *= -1 } - v54 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v54 + v56 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v56 this.TotalVotingPower = int64(r.Int63()) if r.Intn(2) == 0 { this.TotalVotingPower *= -1 @@ -8394,9 +8542,9 @@ func randUTF8RuneTypes(r randyTypes) rune { return rune(ru + 61) } func randStringTypes(r randyTypes) string { - v55 := r.Intn(100) - tmps := make([]rune, v55) - for i := 0; i < v55; i++ { + v57 := r.Intn(100) + tmps := make([]rune, v57) + for i := 0; i < v57; i++ { tmps[i] = randUTF8RuneTypes(r) } return string(tmps) @@ -8418,11 +8566,11 @@ func randFieldTypes(dAtA []byte, r randyTypes, fieldNumber int, wire int) []byte switch wire { case 0: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) - v56 := r.Int63() + v58 := r.Int63() if r.Intn(2) == 0 { - v56 *= -1 + v58 *= -1 } - dAtA = encodeVarintPopulateTypes(dAtA, uint64(v56)) + dAtA = encodeVarintPopulateTypes(dAtA, uint64(v58)) case 1: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) dAtA = append(dAtA, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256))) @@ -9136,8 +9284,8 @@ func (m *ResponseBeginBlock) Size() (n int) { } var l int _ = l - if len(m.Tags) > 0 { - for _, e := range m.Tags { + if len(m.Events) > 0 { + for _, e := range m.Events { l = e.Size() n += 1 + l + sovTypes(uint64(l)) } @@ -9175,8 +9323,8 @@ func (m *ResponseCheckTx) Size() (n int) { if m.GasUsed != 0 { n += 1 + sovTypes(uint64(m.GasUsed)) } - if len(m.Tags) > 0 { - for _, e := range m.Tags { + if len(m.Events) > 0 { + for _, e := range m.Events { l = e.Size() n += 1 + l + sovTypes(uint64(l)) } @@ -9218,8 +9366,8 @@ func (m *ResponseDeliverTx) Size() (n int) { if m.GasUsed != 0 { n += 1 + sovTypes(uint64(m.GasUsed)) } - if len(m.Tags) > 0 { - for _, e := range m.Tags { + if len(m.Events) > 0 { + for _, e := range m.Events { l = e.Size() n += 1 + l + sovTypes(uint64(l)) } @@ -9250,8 +9398,8 @@ func (m *ResponseEndBlock) Size() (n int) { l = m.ConsensusParamUpdates.Size() n += 1 + l + sovTypes(uint64(l)) } - if len(m.Tags) > 0 { - for _, e := range m.Tags { + if len(m.Events) > 0 { + for _, e := range m.Events { l = e.Size() n += 1 + l + sovTypes(uint64(l)) } @@ -9374,6 +9522,28 @@ func (m *LastCommitInfo) Size() (n int) { return n } +func (m *Event) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + l = len(m.Type) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + if len(m.Attributes) > 0 { + for _, e := range m.Attributes { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + func (m *Header) Size() (n int) { if m == nil { return 0 @@ -12570,7 +12740,7 @@ func (m *ResponseBeginBlock) Unmarshal(dAtA []byte) error { switch fieldNum { case 1: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Events", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -12594,8 +12764,8 @@ func (m *ResponseBeginBlock) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Tags = append(m.Tags, common.KVPair{}) - if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.Events = append(m.Events, Event{}) + if err := m.Events[len(m.Events)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex @@ -12798,7 +12968,7 @@ func (m *ResponseCheckTx) Unmarshal(dAtA []byte) error { } case 7: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Events", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -12822,8 +12992,8 @@ func (m *ResponseCheckTx) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Tags = append(m.Tags, common.KVPair{}) - if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.Events = append(m.Events, Event{}) + if err := m.Events[len(m.Events)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex @@ -13055,7 +13225,7 @@ func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { } case 7: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Events", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -13079,8 +13249,8 @@ func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Tags = append(m.Tags, common.KVPair{}) - if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.Events = append(m.Events, Event{}) + if err := m.Events[len(m.Events)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex @@ -13230,7 +13400,7 @@ func (m *ResponseEndBlock) Unmarshal(dAtA []byte) error { iNdEx = postIndex case 3: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Events", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -13254,8 +13424,8 @@ func (m *ResponseEndBlock) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Tags = append(m.Tags, common.KVPair{}) - if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.Events = append(m.Events, Event{}) + if err := m.Events[len(m.Events)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex @@ -13853,6 +14023,117 @@ func (m *LastCommitInfo) Unmarshal(dAtA []byte) error { } return nil } +func (m *Event) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: Event: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: Event: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Type = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Attributes", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Attributes = append(m.Attributes, common.KVPair{}) + if err := m.Attributes[len(m.Attributes)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *Header) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -15357,149 +15638,152 @@ var ( ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") ) -func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7e896a7c04915591) } +func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_62f0c59aeb977f78) } func init() { - golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7e896a7c04915591) -} - -var fileDescriptor_types_7e896a7c04915591 = []byte{ - // 2203 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xdc, 0x58, 0xcf, 0x73, 0x1c, 0x47, - 0xf5, 0xd7, 0xec, 0xef, 0x79, 0xab, 0xfd, 0xe1, 0xb6, 0x6c, 0xaf, 0xf7, 0x9b, 0xaf, 0xe4, 0x1a, - 0x43, 0x22, 0x11, 0x67, 0x95, 0x28, 0x98, 0x92, 0xe3, 0x40, 0x95, 0x56, 0x36, 0x48, 0x95, 0x00, - 0x62, 0x6c, 0x8b, 0x0b, 0x55, 0x53, 0xbd, 0x3b, 0xad, 0xdd, 0x29, 0xed, 0xce, 0x4c, 0x66, 0x7a, - 0x95, 0x15, 0x47, 0xce, 0x39, 0xe4, 0xc0, 0x9f, 0xc0, 0x81, 0x3f, 0x21, 0x47, 0x4e, 0x54, 0x8e, - 0x1c, 0x38, 0x1b, 0x10, 0xc5, 0x01, 0xae, 0x14, 0x55, 0x1c, 0xa9, 0x7e, 0xdd, 0xf3, 0x53, 0xb3, - 0x26, 0x0e, 0x9c, 0xb8, 0x48, 0xd3, 0xfd, 0x3e, 0xaf, 0x7f, 0xbc, 0x7d, 0xef, 0x7d, 0xde, 0x6b, - 0xb8, 0x4d, 0x47, 0x63, 0x67, 0x97, 0x5f, 0xfa, 0x2c, 0x94, 0x7f, 0x07, 0x7e, 0xe0, 0x71, 0x8f, - 0x54, 0x71, 0xd0, 0x7f, 0x67, 0xe2, 0xf0, 0xe9, 0x62, 0x34, 0x18, 0x7b, 0xf3, 0xdd, 0x89, 0x37, - 0xf1, 0x76, 0x51, 0x3a, 0x5a, 0x9c, 0xe1, 0x08, 0x07, 0xf8, 0x25, 0xb5, 0xfa, 0x8f, 0x53, 0x70, - 0xce, 0x5c, 0x9b, 0x05, 0x73, 0xc7, 0xe5, 0xe9, 0xcf, 0x71, 0x70, 0xe9, 0x73, 0x6f, 0x77, 0xce, - 0x82, 0xf3, 0x19, 0x53, 0xff, 0x94, 0xf2, 0xfe, 0xbf, 0x55, 0x9e, 0x39, 0xa3, 0x70, 0x77, 0xec, - 0xcd, 0xe7, 0x9e, 0x9b, 0x3e, 0x6c, 0x7f, 0x6b, 0xe2, 0x79, 0x93, 0x19, 0x4b, 0x0e, 0xc7, 0x9d, - 0x39, 0x0b, 0x39, 0x9d, 0xfb, 0x12, 0x60, 0xfc, 0xb6, 0x02, 0x75, 0x93, 0x7d, 0xb2, 0x60, 0x21, - 0x27, 0xdb, 0x50, 0x61, 0xe3, 0xa9, 0xd7, 0x2b, 0xdd, 0xd3, 0xb6, 0x9b, 0x7b, 0x64, 0x20, 0x17, - 0x52, 0xd2, 0xa7, 0xe3, 0xa9, 0x77, 0xb4, 0x66, 0x22, 0x82, 0xbc, 0x0d, 0xd5, 0xb3, 0xd9, 0x22, - 0x9c, 0xf6, 0xca, 0x08, 0xbd, 0x99, 0x85, 0x7e, 0x5f, 0x88, 0x8e, 0xd6, 0x4c, 0x89, 0x11, 0xcb, - 0x3a, 0xee, 0x99, 0xd7, 0xab, 0x14, 0x2d, 0x7b, 0xec, 0x9e, 0xe1, 0xb2, 0x02, 0x41, 0xf6, 0x01, - 0x42, 0xc6, 0x2d, 0xcf, 0xe7, 0x8e, 0xe7, 0xf6, 0xaa, 0x88, 0xbf, 0x93, 0xc5, 0x3f, 0x63, 0xfc, - 0xc7, 0x28, 0x3e, 0x5a, 0x33, 0xf5, 0x30, 0x1a, 0x08, 0x4d, 0xc7, 0x75, 0xb8, 0x35, 0x9e, 0x52, - 0xc7, 0xed, 0xd5, 0x8a, 0x34, 0x8f, 0x5d, 0x87, 0x1f, 0x0a, 0xb1, 0xd0, 0x74, 0xa2, 0x81, 0xb8, - 0xca, 0x27, 0x0b, 0x16, 0x5c, 0xf6, 0xea, 0x45, 0x57, 0xf9, 0x89, 0x10, 0x89, 0xab, 0x20, 0x86, - 0x3c, 0x86, 0xe6, 0x88, 0x4d, 0x1c, 0xd7, 0x1a, 0xcd, 0xbc, 0xf1, 0x79, 0xaf, 0x81, 0x2a, 0xbd, - 0xac, 0xca, 0x50, 0x00, 0x86, 0x42, 0x7e, 0xb4, 0x66, 0xc2, 0x28, 0x1e, 0x91, 0x3d, 0x68, 0x8c, - 0xa7, 0x6c, 0x7c, 0x6e, 0xf1, 0x65, 0x4f, 0x47, 0xcd, 0x5b, 0x59, 0xcd, 0x43, 0x21, 0x7d, 0xbe, - 0x3c, 0x5a, 0x33, 0xeb, 0x63, 0xf9, 0x49, 0x1e, 0x82, 0xce, 0x5c, 0x5b, 0x6d, 0xd7, 0x44, 0xa5, - 0xdb, 0xb9, 0xdf, 0xc5, 0xb5, 0xa3, 0xcd, 0x1a, 0x4c, 0x7d, 0x93, 0x01, 0xd4, 0x84, 0x33, 0x38, - 0xbc, 0xb7, 0x8e, 0x3a, 0x1b, 0xb9, 0x8d, 0x50, 0x76, 0xb4, 0x66, 0x2a, 0x94, 0x30, 0x9f, 0xcd, - 0x66, 0xce, 0x05, 0x0b, 0xc4, 0xe1, 0x6e, 0x16, 0x99, 0xef, 0x89, 0x94, 0xe3, 0xf1, 0x74, 0x3b, - 0x1a, 0x0c, 0xeb, 0x50, 0xbd, 0xa0, 0xb3, 0x05, 0x33, 0xde, 0x82, 0x66, 0xca, 0x53, 0x48, 0x0f, - 0xea, 0x73, 0x16, 0x86, 0x74, 0xc2, 0x7a, 0xda, 0x3d, 0x6d, 0x5b, 0x37, 0xa3, 0xa1, 0xd1, 0x86, - 0xf5, 0xb4, 0x9f, 0x18, 0xf3, 0x58, 0x51, 0xf8, 0x82, 0x50, 0xbc, 0x60, 0x41, 0x28, 0x1c, 0x40, - 0x29, 0xaa, 0x21, 0xb9, 0x0f, 0x2d, 0xb4, 0x83, 0x15, 0xc9, 0x85, 0x9f, 0x56, 0xcc, 0x75, 0x9c, - 0x3c, 0x55, 0xa0, 0x2d, 0x68, 0xfa, 0x7b, 0x7e, 0x0c, 0x29, 0x23, 0x04, 0xfc, 0x3d, 0x5f, 0x01, - 0x8c, 0x0f, 0xa0, 0x9b, 0x77, 0x25, 0xd2, 0x85, 0xf2, 0x39, 0xbb, 0x54, 0xfb, 0x89, 0x4f, 0xb2, - 0xa1, 0xae, 0x85, 0x7b, 0xe8, 0xa6, 0xba, 0xe3, 0xe7, 0xa5, 0x58, 0x39, 0xf6, 0x26, 0xb2, 0x0f, - 0x15, 0x11, 0x54, 0xa8, 0xdd, 0xdc, 0xeb, 0x0f, 0x64, 0xc4, 0x0d, 0xa2, 0x88, 0x1b, 0x3c, 0x8f, - 0x22, 0x6e, 0xd8, 0xf8, 0xf2, 0xe5, 0xd6, 0xda, 0xe7, 0x7f, 0xd8, 0xd2, 0x4c, 0xd4, 0x20, 0x77, - 0x85, 0x43, 0x50, 0xc7, 0xb5, 0x1c, 0x5b, 0xed, 0x53, 0xc7, 0xf1, 0xb1, 0x4d, 0x0e, 0xa0, 0x3b, - 0xf6, 0xdc, 0x90, 0xb9, 0xe1, 0x22, 0xb4, 0x7c, 0x1a, 0xd0, 0x79, 0xa8, 0x62, 0x2d, 0xfa, 0xf9, - 0x0f, 0x23, 0xf1, 0x09, 0x4a, 0xcd, 0xce, 0x38, 0x3b, 0x41, 0x3e, 0x04, 0xb8, 0xa0, 0x33, 0xc7, - 0xa6, 0xdc, 0x0b, 0xc2, 0x5e, 0xe5, 0x5e, 0x39, 0xa5, 0x7c, 0x1a, 0x09, 0x5e, 0xf8, 0x36, 0xe5, - 0x6c, 0x58, 0x11, 0x27, 0x33, 0x53, 0x78, 0xf2, 0x26, 0x74, 0xa8, 0xef, 0x5b, 0x21, 0xa7, 0x9c, - 0x59, 0xa3, 0x4b, 0xce, 0x42, 0x8c, 0xc7, 0x75, 0xb3, 0x45, 0x7d, 0xff, 0x99, 0x98, 0x1d, 0x8a, - 0x49, 0xc3, 0x8e, 0x7f, 0x4d, 0x0c, 0x15, 0x42, 0xa0, 0x62, 0x53, 0x4e, 0xd1, 0x1a, 0xeb, 0x26, - 0x7e, 0x8b, 0x39, 0x9f, 0xf2, 0xa9, 0xba, 0x23, 0x7e, 0x93, 0xdb, 0x50, 0x9b, 0x32, 0x67, 0x32, - 0xe5, 0x78, 0xad, 0xb2, 0xa9, 0x46, 0xc2, 0xf0, 0x7e, 0xe0, 0x5d, 0x30, 0xcc, 0x16, 0x0d, 0x53, - 0x0e, 0x8c, 0xbf, 0x68, 0x70, 0xe3, 0x5a, 0x78, 0x89, 0x75, 0xa7, 0x34, 0x9c, 0x46, 0x7b, 0x89, - 0x6f, 0xf2, 0xb6, 0x58, 0x97, 0xda, 0x2c, 0x50, 0x59, 0xac, 0xa5, 0x6e, 0x7c, 0x84, 0x93, 0xea, - 0xa2, 0x0a, 0x42, 0x9e, 0x42, 0x77, 0x46, 0x43, 0x6e, 0xc9, 0x28, 0xb0, 0x30, 0x4b, 0x95, 0x33, - 0x91, 0xf9, 0x31, 0x8d, 0xa2, 0x45, 0x38, 0xa7, 0x52, 0x6f, 0xcf, 0x32, 0xb3, 0xe4, 0x08, 0x36, - 0x46, 0x97, 0x3f, 0xa7, 0x2e, 0x77, 0x5c, 0x66, 0x5d, 0xb3, 0x79, 0x47, 0x2d, 0xf5, 0xf4, 0xc2, - 0xb1, 0x99, 0x3b, 0x8e, 0x8c, 0x7d, 0x33, 0x56, 0x89, 0x7f, 0x8c, 0xd0, 0xb8, 0x07, 0xed, 0x6c, - 0x2e, 0x20, 0x6d, 0x28, 0xf1, 0xa5, 0xba, 0x61, 0x89, 0x2f, 0x0d, 0x23, 0xf6, 0xc0, 0x38, 0x20, - 0xaf, 0x61, 0x76, 0xa0, 0x93, 0x4b, 0x0e, 0x29, 0x73, 0x6b, 0x69, 0x73, 0x1b, 0x1d, 0x68, 0x65, - 0x72, 0x82, 0xf1, 0x59, 0x15, 0x1a, 0x26, 0x0b, 0x7d, 0xe1, 0x4c, 0x64, 0x1f, 0x74, 0xb6, 0x1c, - 0x33, 0x99, 0x8e, 0xb5, 0x5c, 0xb2, 0x93, 0x98, 0xa7, 0x91, 0x5c, 0xa4, 0x85, 0x18, 0x4c, 0x76, - 0x32, 0x54, 0x72, 0x33, 0xaf, 0x94, 0xe6, 0x92, 0x07, 0x59, 0x2e, 0xd9, 0xc8, 0x61, 0x73, 0x64, - 0xb2, 0x93, 0x21, 0x93, 0xfc, 0xc2, 0x19, 0x36, 0x79, 0x54, 0xc0, 0x26, 0xf9, 0xe3, 0xaf, 0xa0, - 0x93, 0x47, 0x05, 0x74, 0xd2, 0xbb, 0xb6, 0x57, 0x21, 0x9f, 0x3c, 0xc8, 0xf2, 0x49, 0xfe, 0x3a, - 0x39, 0x42, 0xf9, 0xb0, 0x88, 0x50, 0xee, 0xe6, 0x74, 0x56, 0x32, 0xca, 0xfb, 0xd7, 0x18, 0xe5, - 0x76, 0x4e, 0xb5, 0x80, 0x52, 0x1e, 0x65, 0x72, 0x3d, 0x14, 0xde, 0xad, 0x38, 0xd9, 0x93, 0xef, - 0x5c, 0x67, 0xa3, 0x3b, 0xf9, 0x9f, 0xb6, 0x88, 0x8e, 0x76, 0x73, 0x74, 0x74, 0x2b, 0x7f, 0xca, - 0x1c, 0x1f, 0x25, 0xac, 0xb2, 0x23, 0xe2, 0x3e, 0xe7, 0x69, 0x22, 0x47, 0xb0, 0x20, 0xf0, 0x02, - 0x95, 0xb0, 0xe5, 0xc0, 0xd8, 0x16, 0x99, 0x28, 0xf1, 0xaf, 0x57, 0x30, 0x10, 0x3a, 0x7d, 0xca, + golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_62f0c59aeb977f78) +} + +var fileDescriptor_types_62f0c59aeb977f78 = []byte{ + // 2241 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x58, 0x4b, 0x73, 0x1c, 0x49, + 0xf1, 0x57, 0xcf, 0xbb, 0x73, 0x34, 0x0f, 0x97, 0x65, 0x7b, 0x3c, 0x7f, 0xff, 0x25, 0x47, 0x1b, + 0x76, 0x25, 0xd6, 0x3b, 0xda, 0xd5, 0x62, 0x42, 0xc6, 0xcb, 0x46, 0x68, 0x6c, 0x83, 0x14, 0x6b, + 0x40, 0xb4, 0x6d, 0x71, 0x21, 0xa2, 0xa3, 0x66, 0xba, 0x3c, 0xd3, 0xe1, 0x99, 0xee, 0xde, 0xee, + 0x9a, 0xd9, 0x11, 0x47, 0xce, 0x7b, 0xd8, 0x03, 0x1f, 0x81, 0x03, 0x1f, 0x61, 0x8f, 0x9c, 0x88, + 0x3d, 0x72, 0xe0, 0x6c, 0x40, 0x04, 0x17, 0x22, 0x38, 0x03, 0x37, 0xa2, 0xb2, 0xaa, 0x9f, 0xea, + 0x31, 0xbb, 0x86, 0x1b, 0x17, 0xa9, 0xab, 0xf2, 0x97, 0xf5, 0xc8, 0xc9, 0xcc, 0x5f, 0x66, 0xc1, + 0x75, 0x3a, 0x1a, 0x3b, 0xfb, 0xfc, 0xdc, 0x67, 0xa1, 0xfc, 0x3b, 0xf0, 0x03, 0x8f, 0x7b, 0xa4, + 0x8a, 0x83, 0xfe, 0xbb, 0x13, 0x87, 0x4f, 0x17, 0xa3, 0xc1, 0xd8, 0x9b, 0xef, 0x4f, 0xbc, 0x89, + 0xb7, 0x8f, 0xd2, 0xd1, 0xe2, 0x05, 0x8e, 0x70, 0x80, 0x5f, 0x52, 0xab, 0xff, 0x20, 0x05, 0xe7, + 0xcc, 0xb5, 0x59, 0x30, 0x77, 0x5c, 0x9e, 0xfe, 0x1c, 0x07, 0xe7, 0x3e, 0xf7, 0xf6, 0xe7, 0x2c, + 0x78, 0x39, 0x63, 0xea, 0x9f, 0x52, 0x3e, 0xfc, 0xb7, 0xca, 0x33, 0x67, 0x14, 0xee, 0x8f, 0xbd, + 0xf9, 0xdc, 0x73, 0xd3, 0x87, 0xed, 0xef, 0x4c, 0x3c, 0x6f, 0x32, 0x63, 0xc9, 0xe1, 0xb8, 0x33, + 0x67, 0x21, 0xa7, 0x73, 0x5f, 0x02, 0x8c, 0xdf, 0x56, 0xa0, 0x6e, 0xb2, 0x4f, 0x16, 0x2c, 0xe4, + 0x64, 0x17, 0x2a, 0x6c, 0x3c, 0xf5, 0x7a, 0xa5, 0xdb, 0xda, 0x6e, 0xf3, 0x80, 0x0c, 0xe4, 0x42, + 0x4a, 0xfa, 0x78, 0x3c, 0xf5, 0x8e, 0x37, 0x4c, 0x44, 0x90, 0x77, 0xa0, 0xfa, 0x62, 0xb6, 0x08, + 0xa7, 0xbd, 0x32, 0x42, 0xaf, 0x66, 0xa1, 0xdf, 0x17, 0xa2, 0xe3, 0x0d, 0x53, 0x62, 0xc4, 0xb2, + 0x8e, 0xfb, 0xc2, 0xeb, 0x55, 0x8a, 0x96, 0x3d, 0x71, 0x5f, 0xe0, 0xb2, 0x02, 0x41, 0x0e, 0x01, + 0x42, 0xc6, 0x2d, 0xcf, 0xe7, 0x8e, 0xe7, 0xf6, 0xaa, 0x88, 0xbf, 0x91, 0xc5, 0x3f, 0x65, 0xfc, + 0xc7, 0x28, 0x3e, 0xde, 0x30, 0xf5, 0x30, 0x1a, 0x08, 0x4d, 0xc7, 0x75, 0xb8, 0x35, 0x9e, 0x52, + 0xc7, 0xed, 0xd5, 0x8a, 0x34, 0x4f, 0x5c, 0x87, 0x3f, 0x14, 0x62, 0xa1, 0xe9, 0x44, 0x03, 0x71, + 0x95, 0x4f, 0x16, 0x2c, 0x38, 0xef, 0xd5, 0x8b, 0xae, 0xf2, 0x13, 0x21, 0x12, 0x57, 0x41, 0x0c, + 0x79, 0x00, 0xcd, 0x11, 0x9b, 0x38, 0xae, 0x35, 0x9a, 0x79, 0xe3, 0x97, 0xbd, 0x06, 0xaa, 0xf4, + 0xb2, 0x2a, 0x43, 0x01, 0x18, 0x0a, 0xf9, 0xf1, 0x86, 0x09, 0xa3, 0x78, 0x44, 0x0e, 0xa0, 0x31, + 0x9e, 0xb2, 0xf1, 0x4b, 0x8b, 0xaf, 0x7a, 0x3a, 0x6a, 0x5e, 0xcb, 0x6a, 0x3e, 0x14, 0xd2, 0x67, + 0xab, 0xe3, 0x0d, 0xb3, 0x3e, 0x96, 0x9f, 0xe4, 0x1e, 0xe8, 0xcc, 0xb5, 0xd5, 0x76, 0x4d, 0x54, + 0xba, 0x9e, 0xfb, 0x5d, 0x5c, 0x3b, 0xda, 0xac, 0xc1, 0xd4, 0x37, 0x19, 0x40, 0x4d, 0x38, 0x83, + 0xc3, 0x7b, 0x9b, 0xa8, 0xb3, 0x95, 0xdb, 0x08, 0x65, 0xc7, 0x1b, 0xa6, 0x42, 0x09, 0xf3, 0xd9, + 0x6c, 0xe6, 0x2c, 0x59, 0x20, 0x0e, 0x77, 0xb5, 0xc8, 0x7c, 0x8f, 0xa4, 0x1c, 0x8f, 0xa7, 0xdb, + 0xd1, 0x60, 0x58, 0x87, 0xea, 0x92, 0xce, 0x16, 0xcc, 0x78, 0x1b, 0x9a, 0x29, 0x4f, 0x21, 0x3d, + 0xa8, 0xcf, 0x59, 0x18, 0xd2, 0x09, 0xeb, 0x69, 0xb7, 0xb5, 0x5d, 0xdd, 0x8c, 0x86, 0x46, 0x1b, + 0x36, 0xd3, 0x7e, 0x62, 0xcc, 0x63, 0x45, 0xe1, 0x0b, 0x42, 0x71, 0xc9, 0x82, 0x50, 0x38, 0x80, + 0x52, 0x54, 0x43, 0x72, 0x07, 0x5a, 0x68, 0x07, 0x2b, 0x92, 0x0b, 0x3f, 0xad, 0x98, 0x9b, 0x38, + 0x79, 0xa6, 0x40, 0x3b, 0xd0, 0xf4, 0x0f, 0xfc, 0x18, 0x52, 0x46, 0x08, 0xf8, 0x07, 0xbe, 0x02, + 0x18, 0xdf, 0x85, 0x6e, 0xde, 0x95, 0x48, 0x17, 0xca, 0x2f, 0xd9, 0xb9, 0xda, 0x4f, 0x7c, 0x92, + 0x2d, 0x75, 0x2d, 0xdc, 0x43, 0x37, 0xd5, 0x1d, 0x3f, 0x2f, 0xc5, 0xca, 0xb1, 0x37, 0x91, 0x43, + 0xa8, 0x88, 0xa0, 0x42, 0xed, 0xe6, 0x41, 0x7f, 0x20, 0x23, 0x6e, 0x10, 0x45, 0xdc, 0xe0, 0x59, + 0x14, 0x71, 0xc3, 0xc6, 0x97, 0xaf, 0x76, 0x36, 0x3e, 0xff, 0xc3, 0x8e, 0x66, 0xa2, 0x06, 0xb9, + 0x29, 0x1c, 0x82, 0x3a, 0xae, 0xe5, 0xd8, 0x6a, 0x9f, 0x3a, 0x8e, 0x4f, 0x6c, 0x72, 0x04, 0xdd, + 0xb1, 0xe7, 0x86, 0xcc, 0x0d, 0x17, 0xa1, 0xe5, 0xd3, 0x80, 0xce, 0x43, 0x15, 0x6b, 0xd1, 0xcf, + 0xff, 0x30, 0x12, 0x9f, 0xa2, 0xd4, 0xec, 0x8c, 0xb3, 0x13, 0xe4, 0x43, 0x80, 0x25, 0x9d, 0x39, + 0x36, 0xe5, 0x5e, 0x10, 0xf6, 0x2a, 0xb7, 0xcb, 0x29, 0xe5, 0xb3, 0x48, 0xf0, 0xdc, 0xb7, 0x29, + 0x67, 0xc3, 0x8a, 0x38, 0x99, 0x99, 0xc2, 0x93, 0xb7, 0xa0, 0x43, 0x7d, 0xdf, 0x0a, 0x39, 0xe5, + 0xcc, 0x1a, 0x9d, 0x73, 0x16, 0x62, 0x3c, 0x6e, 0x9a, 0x2d, 0xea, 0xfb, 0x4f, 0xc5, 0xec, 0x50, + 0x4c, 0x1a, 0x76, 0xfc, 0x6b, 0x62, 0xa8, 0x10, 0x02, 0x15, 0x9b, 0x72, 0x8a, 0xd6, 0xd8, 0x34, + 0xf1, 0x5b, 0xcc, 0xf9, 0x94, 0x4f, 0xd5, 0x1d, 0xf1, 0x9b, 0x5c, 0x87, 0xda, 0x94, 0x39, 0x93, + 0x29, 0xc7, 0x6b, 0x95, 0x4d, 0x35, 0x12, 0x86, 0xf7, 0x03, 0x6f, 0xc9, 0x30, 0x5b, 0x34, 0x4c, + 0x39, 0x30, 0xfe, 0xa2, 0xc1, 0x95, 0x4b, 0xe1, 0x25, 0xd6, 0x9d, 0xd2, 0x70, 0x1a, 0xed, 0x25, + 0xbe, 0xc9, 0x3b, 0x62, 0x5d, 0x6a, 0xb3, 0x40, 0x65, 0xb1, 0x96, 0xba, 0xf1, 0x31, 0x4e, 0xaa, + 0x8b, 0x2a, 0x08, 0x79, 0x0c, 0xdd, 0x19, 0x0d, 0xb9, 0x25, 0xa3, 0xc0, 0xc2, 0x2c, 0x55, 0xce, + 0x44, 0xe6, 0x13, 0x1a, 0x45, 0x8b, 0x70, 0x4e, 0xa5, 0xde, 0x9e, 0x65, 0x66, 0xc9, 0x31, 0x6c, + 0x8d, 0xce, 0x7f, 0x4e, 0x5d, 0xee, 0xb8, 0xcc, 0xba, 0x64, 0xf3, 0x8e, 0x5a, 0xea, 0xf1, 0xd2, + 0xb1, 0x99, 0x3b, 0x8e, 0x8c, 0x7d, 0x35, 0x56, 0x89, 0x7f, 0x8c, 0xd0, 0xb8, 0x0d, 0xed, 0x6c, + 0x2e, 0x20, 0x6d, 0x28, 0xf1, 0x95, 0xba, 0x61, 0x89, 0xaf, 0x0c, 0x23, 0xf6, 0xc0, 0x38, 0x20, + 0x2f, 0x61, 0xf6, 0xa0, 0x93, 0x4b, 0x0e, 0x29, 0x73, 0x6b, 0x69, 0x73, 0x1b, 0x1d, 0x68, 0x65, + 0x72, 0x82, 0xf1, 0x59, 0x15, 0x1a, 0x26, 0x0b, 0x7d, 0xe1, 0x4c, 0xe4, 0x10, 0x74, 0xb6, 0x1a, + 0x33, 0x99, 0x8e, 0xb5, 0x5c, 0xb2, 0x93, 0x98, 0xc7, 0x91, 0x5c, 0xa4, 0x85, 0x18, 0x4c, 0xf6, + 0x32, 0x54, 0x72, 0x35, 0xaf, 0x94, 0xe6, 0x92, 0xbb, 0x59, 0x2e, 0xd9, 0xca, 0x61, 0x73, 0x64, + 0xb2, 0x97, 0x21, 0x93, 0xfc, 0xc2, 0x19, 0x36, 0xb9, 0x5f, 0xc0, 0x26, 0xf9, 0xe3, 0xaf, 0xa1, + 0x93, 0xfb, 0x05, 0x74, 0xd2, 0xbb, 0xb4, 0x57, 0x21, 0x9f, 0xdc, 0xcd, 0xf2, 0x49, 0xfe, 0x3a, + 0x39, 0x42, 0xf9, 0xb0, 0x88, 0x50, 0x6e, 0xe6, 0x74, 0xd6, 0x32, 0xca, 0x07, 0x97, 0x18, 0xe5, + 0x7a, 0x4e, 0xb5, 0x80, 0x52, 0xee, 0x67, 0x72, 0x3d, 0x14, 0xde, 0xad, 0x38, 0xd9, 0x93, 0xef, + 0x5c, 0x66, 0xa3, 0x1b, 0xf9, 0x9f, 0xb6, 0x88, 0x8e, 0xf6, 0x73, 0x74, 0x74, 0x2d, 0x7f, 0xca, + 0x1c, 0x1f, 0x25, 0xac, 0xb2, 0x27, 0xe2, 0x3e, 0xe7, 0x69, 0x22, 0x47, 0xb0, 0x20, 0xf0, 0x02, + 0x95, 0xb0, 0xe5, 0xc0, 0xd8, 0x15, 0x99, 0x28, 0xf1, 0xaf, 0xd7, 0x30, 0x10, 0x3a, 0x7d, 0xca, 0xbb, 0x8c, 0x2f, 0xb4, 0x44, 0x17, 0x23, 0x3a, 0x9d, 0xc5, 0x74, 0x95, 0xc5, 0x52, 0xc4, 0x54, - 0xca, 0x12, 0xd3, 0x16, 0x34, 0x45, 0xae, 0xcc, 0x71, 0x0e, 0xf5, 0x23, 0xce, 0x21, 0xdf, 0x82, - 0x1b, 0x98, 0x67, 0x24, 0x7d, 0xa9, 0x40, 0xac, 0x60, 0x20, 0x76, 0x84, 0x40, 0x5a, 0x4c, 0x26, - 0xc0, 0x77, 0xe0, 0x66, 0x0a, 0x2b, 0xd6, 0xc5, 0x1c, 0x27, 0x93, 0x6f, 0x37, 0x46, 0x1f, 0xf8, - 0xfe, 0x11, 0x0d, 0xa7, 0xc6, 0x0f, 0x13, 0x03, 0x25, 0x7c, 0x46, 0xa0, 0x32, 0xf6, 0x6c, 0x79, + 0xca, 0x12, 0xd3, 0x0e, 0x34, 0x45, 0xae, 0xcc, 0x71, 0x0e, 0xf5, 0x23, 0xce, 0x21, 0xdf, 0x82, + 0x2b, 0x98, 0x67, 0x24, 0x7d, 0xa9, 0x40, 0xac, 0x60, 0x20, 0x76, 0x84, 0x40, 0x5a, 0x4c, 0x26, + 0xc0, 0x77, 0xe1, 0x6a, 0x0a, 0x2b, 0xd6, 0xc5, 0x1c, 0x27, 0x93, 0x6f, 0x37, 0x46, 0x1f, 0xf9, + 0xfe, 0x31, 0x0d, 0xa7, 0xc6, 0x0f, 0x13, 0x03, 0x25, 0x7c, 0x46, 0xa0, 0x32, 0xf6, 0x6c, 0x79, 0xef, 0x96, 0x89, 0xdf, 0x82, 0xe3, 0x66, 0xde, 0x04, 0x0f, 0xa7, 0x9b, 0xe2, 0x53, 0xa0, 0xe2, 0x50, 0xd2, 0x65, 0xcc, 0x18, 0xbf, 0xd4, 0x92, 0xf5, 0x12, 0x8a, 0x2b, 0x62, 0x23, 0xed, 0x3f, - 0x61, 0xa3, 0xd2, 0xeb, 0xb1, 0x91, 0x71, 0xa5, 0x25, 0x3f, 0x59, 0xcc, 0x33, 0x5f, 0xef, 0x8a, - 0xc2, 0x7b, 0x1c, 0xd7, 0x66, 0x4b, 0x34, 0x69, 0xd9, 0x94, 0x83, 0xa8, 0x04, 0xa8, 0xa1, 0x99, - 0xb3, 0x25, 0x40, 0x1d, 0xe7, 0xe4, 0x80, 0xdc, 0x47, 0x7e, 0xf2, 0xce, 0x54, 0xa8, 0xb6, 0x06, - 0xaa, 0x50, 0x3f, 0x11, 0x93, 0xa6, 0x94, 0xa5, 0xb2, 0xad, 0x9e, 0x21, 0xb7, 0x37, 0x40, 0x17, - 0x07, 0x0d, 0x7d, 0x3a, 0x66, 0x18, 0x79, 0xba, 0x99, 0x4c, 0x18, 0x27, 0x40, 0xae, 0x47, 0x3c, - 0xf9, 0x00, 0x2a, 0x9c, 0x4e, 0x84, 0xbd, 0x85, 0xc9, 0xda, 0x03, 0x59, 0xe4, 0x0f, 0x3e, 0x3a, - 0x3d, 0xa1, 0x4e, 0x30, 0xbc, 0x2d, 0x4c, 0xf5, 0xb7, 0x97, 0x5b, 0x6d, 0x81, 0x79, 0xe0, 0xcd, - 0x1d, 0xce, 0xe6, 0x3e, 0xbf, 0x34, 0x51, 0xc7, 0xf8, 0xbb, 0x26, 0x98, 0x20, 0x93, 0x09, 0x0a, - 0x0d, 0x17, 0xb9, 0x7b, 0x29, 0x45, 0xda, 0x5f, 0xcd, 0x98, 0xff, 0x0f, 0x30, 0xa1, 0xa1, 0xf5, - 0x29, 0x75, 0x39, 0xb3, 0x95, 0x45, 0xf5, 0x09, 0x0d, 0x7f, 0x8a, 0x13, 0xa2, 0xc2, 0x11, 0xe2, - 0x45, 0xc8, 0x6c, 0x34, 0x6d, 0xd9, 0xac, 0x4f, 0x68, 0xf8, 0x22, 0x64, 0x76, 0x7c, 0xaf, 0xfa, - 0xeb, 0xdf, 0x2b, 0x6b, 0xc7, 0x46, 0xde, 0x8e, 0xff, 0x48, 0xf9, 0x70, 0x42, 0x92, 0xff, 0xfb, - 0xf7, 0xfe, 0xab, 0x26, 0x6a, 0x83, 0x6c, 0x1a, 0x26, 0xc7, 0x70, 0x23, 0x8e, 0x23, 0x6b, 0x81, - 0xf1, 0x15, 0xf9, 0xd2, 0xab, 0xc3, 0xaf, 0x7b, 0x91, 0x9d, 0x0e, 0xc9, 0x8f, 0xe0, 0x4e, 0x2e, - 0x0b, 0xc4, 0x0b, 0x96, 0x5e, 0x99, 0x0c, 0x6e, 0x65, 0x93, 0x41, 0xb4, 0x5e, 0x64, 0x89, 0xf2, - 0xd7, 0xf0, 0xec, 0x6f, 0x88, 0x42, 0x29, 0x4d, 0x1e, 0x45, 0xbf, 0xa5, 0xf1, 0x2b, 0x0d, 0x3a, - 0xb9, 0xc3, 0x90, 0x6d, 0xa8, 0x4a, 0xfe, 0xd2, 0x32, 0xed, 0x28, 0x5a, 0x4b, 0x9d, 0x57, 0x02, - 0xc8, 0x7b, 0xd0, 0x60, 0xaa, 0x66, 0x53, 0x17, 0xbc, 0x95, 0x2b, 0xe5, 0x14, 0x3e, 0x86, 0x91, - 0x6f, 0x83, 0x1e, 0x9b, 0x2d, 0x57, 0xaf, 0xc7, 0x56, 0x56, 0x4a, 0x09, 0xd0, 0x38, 0x84, 0x66, - 0x6a, 0x7b, 0xf2, 0x7f, 0xa0, 0xcf, 0xe9, 0x52, 0x15, 0xdd, 0xb2, 0x5c, 0x6b, 0xcc, 0xe9, 0x12, - 0xeb, 0x6d, 0x72, 0x07, 0xea, 0x42, 0x38, 0xa1, 0xd2, 0xe8, 0x65, 0xb3, 0x36, 0xa7, 0xcb, 0x1f, - 0xd0, 0xd0, 0xd8, 0x81, 0x76, 0xf6, 0x58, 0x11, 0x34, 0x22, 0x40, 0x09, 0x3d, 0x98, 0x30, 0xe3, - 0x21, 0x74, 0x72, 0xa7, 0x21, 0x06, 0xb4, 0xfc, 0xc5, 0xc8, 0x3a, 0x67, 0x97, 0x16, 0x1e, 0x17, - 0x5d, 0x44, 0x37, 0x9b, 0xfe, 0x62, 0xf4, 0x11, 0xbb, 0x7c, 0x2e, 0xa6, 0x8c, 0x67, 0xd0, 0xce, - 0x96, 0xc3, 0x22, 0x45, 0x06, 0xde, 0xc2, 0xb5, 0x71, 0xfd, 0xaa, 0x29, 0x07, 0xa2, 0xa3, 0xbe, - 0xf0, 0xa4, 0x57, 0xa4, 0xeb, 0xdf, 0x53, 0x8f, 0xb3, 0x54, 0x11, 0x2d, 0x31, 0xc6, 0x2f, 0xaa, - 0x50, 0x93, 0xb5, 0x39, 0x19, 0x64, 0x3b, 0x3f, 0xe1, 0x12, 0x4a, 0x53, 0xce, 0x2a, 0xc5, 0x98, - 0x76, 0xdf, 0xcc, 0xb7, 0x4f, 0xc3, 0xe6, 0xd5, 0xcb, 0xad, 0x3a, 0x52, 0xd6, 0xf1, 0x93, 0xa4, - 0x97, 0x5a, 0xd5, 0x6a, 0x44, 0x8d, 0x5b, 0xe5, 0xb5, 0x1b, 0xb7, 0x3b, 0x50, 0x77, 0x17, 0x73, - 0x8b, 0x2f, 0x43, 0x15, 0xfa, 0x35, 0x77, 0x31, 0x7f, 0xbe, 0xc4, 0x9f, 0x8e, 0x7b, 0x9c, 0xce, - 0x50, 0x24, 0x03, 0xbf, 0x81, 0x13, 0x42, 0xb8, 0x0f, 0xad, 0x14, 0xb3, 0x3b, 0xb6, 0xaa, 0x10, - 0xdb, 0x69, 0x0f, 0x3c, 0x7e, 0xa2, 0x6e, 0xd9, 0x8c, 0x99, 0xfe, 0xd8, 0x26, 0xdb, 0xd9, 0x3e, - 0x05, 0x0b, 0x82, 0x06, 0xfa, 0x79, 0xaa, 0x15, 0x11, 0xe5, 0x80, 0x38, 0x80, 0xf0, 0x7c, 0x09, - 0xd1, 0x11, 0xd2, 0x10, 0x13, 0x28, 0x7c, 0x0b, 0x3a, 0x09, 0xa7, 0x4a, 0x08, 0xc8, 0x55, 0x92, - 0x69, 0x04, 0xbe, 0x0b, 0x1b, 0x2e, 0x5b, 0x72, 0x2b, 0x8f, 0x6e, 0x22, 0x9a, 0x08, 0xd9, 0x69, - 0x56, 0xe3, 0x9b, 0xd0, 0x4e, 0x72, 0x03, 0x62, 0xd7, 0x65, 0xb7, 0x18, 0xcf, 0x22, 0xec, 0x2e, - 0x34, 0xe2, 0x8a, 0xa6, 0x85, 0x80, 0x3a, 0x95, 0x85, 0x4c, 0x5c, 0x23, 0x05, 0x2c, 0x5c, 0xcc, - 0xb8, 0x5a, 0xa4, 0x8d, 0x18, 0xac, 0x91, 0x4c, 0x39, 0x8f, 0xd8, 0xfb, 0xd0, 0x8a, 0x42, 0x4e, - 0xe2, 0x3a, 0x88, 0x5b, 0x8f, 0x26, 0x11, 0xb4, 0x03, 0x5d, 0x3f, 0xf0, 0x7c, 0x2f, 0x64, 0x81, - 0x45, 0x6d, 0x3b, 0x60, 0x61, 0xd8, 0xeb, 0xca, 0xf5, 0xa2, 0xf9, 0x03, 0x39, 0x6d, 0xbc, 0x07, - 0xf5, 0xa8, 0x54, 0xdb, 0x80, 0xea, 0x30, 0x4e, 0x0f, 0x15, 0x53, 0x0e, 0x04, 0x29, 0x1c, 0xf8, - 0xbe, 0x7a, 0x70, 0x10, 0x9f, 0xc6, 0xcf, 0xa0, 0xae, 0x7e, 0xb0, 0xc2, 0x36, 0xf4, 0xbb, 0xb0, - 0xee, 0xd3, 0x40, 0x5c, 0x23, 0xdd, 0x8c, 0x46, 0xcd, 0xc0, 0x09, 0x0d, 0xf8, 0x33, 0xc6, 0x33, - 0x3d, 0x69, 0x13, 0xf1, 0x72, 0xca, 0x78, 0x04, 0xad, 0x0c, 0x46, 0x1c, 0x0b, 0xfd, 0x28, 0x8a, - 0x34, 0x1c, 0xc4, 0x3b, 0x97, 0x92, 0x9d, 0x8d, 0xc7, 0xa0, 0xc7, 0xbf, 0x8d, 0xa8, 0x59, 0xa3, - 0xab, 0x6b, 0xca, 0xdc, 0x72, 0x88, 0x7d, 0xb6, 0xf7, 0x29, 0x0b, 0x54, 0x4c, 0xc8, 0x81, 0xf1, - 0x22, 0x95, 0x19, 0x64, 0x9a, 0x26, 0x0f, 0xa0, 0xae, 0x32, 0x83, 0x8a, 0xca, 0xa8, 0xa3, 0x3e, - 0xc1, 0xd4, 0x10, 0x75, 0xd4, 0x32, 0x51, 0x24, 0xcb, 0x96, 0xd2, 0xcb, 0xce, 0xa0, 0x11, 0x45, - 0x7f, 0x36, 0x45, 0xca, 0x15, 0xbb, 0xf9, 0x14, 0xa9, 0x16, 0x4d, 0x80, 0xc2, 0x3b, 0x42, 0x67, - 0xe2, 0x32, 0xdb, 0x4a, 0x42, 0x08, 0xf7, 0x68, 0x98, 0x1d, 0x29, 0xf8, 0x38, 0x8a, 0x17, 0xe3, - 0x5d, 0xa8, 0xc9, 0xb3, 0x09, 0xfb, 0x88, 0x95, 0xa3, 0x32, 0x5e, 0x7c, 0x17, 0xf2, 0xc4, 0xef, - 0x35, 0x68, 0x44, 0xc9, 0xb3, 0x50, 0x29, 0x73, 0xe8, 0xd2, 0x57, 0x3d, 0xf4, 0x7f, 0x3f, 0xf1, - 0x3c, 0x00, 0x22, 0xf3, 0xcb, 0x85, 0xc7, 0x1d, 0x77, 0x62, 0x49, 0x5b, 0xcb, 0x1c, 0xd4, 0x45, - 0xc9, 0x29, 0x0a, 0x4e, 0xc4, 0xfc, 0xde, 0x67, 0x55, 0xe8, 0x1c, 0x0c, 0x0f, 0x8f, 0x0f, 0x7c, - 0x7f, 0xe6, 0x8c, 0x29, 0xb6, 0x06, 0xbb, 0x50, 0xc1, 0xee, 0xa8, 0xe0, 0x75, 0xb7, 0x5f, 0xd4, - 0xa6, 0x93, 0x3d, 0xa8, 0x62, 0x93, 0x44, 0x8a, 0x1e, 0x79, 0xfb, 0x85, 0xdd, 0xba, 0xd8, 0x44, - 0xb6, 0x51, 0xd7, 0xdf, 0x7a, 0xfb, 0x45, 0x2d, 0x3b, 0xf9, 0x1e, 0xe8, 0x49, 0xf7, 0xb2, 0xea, - 0xc5, 0xb7, 0xbf, 0xb2, 0x79, 0x17, 0xfa, 0x49, 0xa5, 0xb7, 0xea, 0xe1, 0xb2, 0xbf, 0xb2, 0xcb, - 0x25, 0xfb, 0x50, 0x8f, 0xea, 0xe3, 0xe2, 0x37, 0xd9, 0xfe, 0x8a, 0xc6, 0x5a, 0x98, 0x47, 0x36, - 0x24, 0x45, 0x0f, 0xc7, 0xfd, 0xc2, 0xee, 0x9f, 0x3c, 0x84, 0x9a, 0x2a, 0x5a, 0x0a, 0xdf, 0x65, - 0xfb, 0xc5, 0xed, 0xb1, 0xb8, 0x64, 0xd2, 0x92, 0xad, 0x7a, 0xdc, 0xee, 0xaf, 0x7c, 0xa6, 0x20, - 0x07, 0x00, 0xa9, 0xbe, 0x62, 0xe5, 0xab, 0x75, 0x7f, 0xf5, 0xf3, 0x03, 0x79, 0x0c, 0x8d, 0xe4, - 0x49, 0xa9, 0xf8, 0x1d, 0xba, 0xbf, 0xea, 0x45, 0x60, 0xf8, 0xc6, 0x3f, 0xff, 0xb4, 0xa9, 0xfd, - 0xfa, 0x6a, 0x53, 0xfb, 0xe2, 0x6a, 0x53, 0xfb, 0xf2, 0x6a, 0x53, 0xfb, 0xdd, 0xd5, 0xa6, 0xf6, - 0xc7, 0xab, 0x4d, 0xed, 0x37, 0x7f, 0xde, 0xd4, 0x46, 0x35, 0x74, 0xff, 0xf7, 0xff, 0x15, 0x00, - 0x00, 0xff, 0xff, 0x38, 0x2d, 0x52, 0x86, 0x77, 0x19, 0x00, 0x00, + 0x61, 0xa3, 0xd2, 0xd7, 0x63, 0x23, 0xe3, 0x42, 0x4b, 0x7e, 0xb2, 0x98, 0x67, 0xde, 0xec, 0x8a, + 0xc2, 0x7b, 0x1c, 0xd7, 0x66, 0x2b, 0x34, 0x69, 0xd9, 0x94, 0x83, 0xa8, 0x04, 0xa8, 0xa1, 0x99, + 0xb3, 0x25, 0x40, 0x1d, 0xe7, 0xe4, 0x80, 0xdc, 0x41, 0x7e, 0xf2, 0x5e, 0xa8, 0x50, 0x6d, 0x0d, + 0x54, 0xa1, 0x7e, 0x2a, 0x26, 0x4d, 0x29, 0x4b, 0x65, 0x5b, 0x3d, 0x43, 0x6e, 0xb7, 0x40, 0x17, + 0x07, 0x0d, 0x7d, 0x3a, 0x66, 0x18, 0x79, 0xba, 0x99, 0x4c, 0x18, 0xcf, 0x80, 0x5c, 0x8e, 0x78, + 0xf2, 0x11, 0xd4, 0xd8, 0x92, 0xb9, 0x5c, 0x58, 0x5c, 0x18, 0x6d, 0x33, 0xa6, 0x13, 0xe6, 0xf2, + 0x61, 0x4f, 0x98, 0xea, 0xaf, 0xaf, 0x76, 0xba, 0x12, 0x73, 0xd7, 0x9b, 0x3b, 0x9c, 0xcd, 0x7d, + 0x7e, 0x6e, 0x2a, 0x2d, 0xe3, 0xef, 0x9a, 0x60, 0x83, 0x4c, 0x36, 0x28, 0x34, 0x5e, 0xe4, 0xf2, + 0xa5, 0x14, 0x71, 0x7f, 0x35, 0x83, 0xfe, 0x3f, 0xc0, 0x84, 0x86, 0xd6, 0xa7, 0xd4, 0xe5, 0xcc, + 0x56, 0x56, 0xd5, 0x27, 0x34, 0xfc, 0x29, 0x4e, 0x88, 0x2a, 0x47, 0x88, 0x17, 0x21, 0xb3, 0xd1, + 0xbc, 0x65, 0xb3, 0x3e, 0xa1, 0xe1, 0xf3, 0x90, 0xd9, 0xa9, 0xbb, 0xd5, 0xdf, 0xe4, 0x6e, 0x59, + 0x7b, 0x36, 0xf2, 0xf6, 0xfc, 0x67, 0xca, 0x97, 0x13, 0xb2, 0xfc, 0xdf, 0xb8, 0xfb, 0xdf, 0x34, + 0x51, 0x27, 0x64, 0x53, 0x32, 0x39, 0x81, 0x2b, 0x71, 0x4c, 0x59, 0x0b, 0x8c, 0xb5, 0xc8, 0xab, + 0x5e, 0x1f, 0x8a, 0xdd, 0x65, 0x76, 0x3a, 0x24, 0x3f, 0x82, 0x1b, 0xb9, 0x8c, 0x10, 0x2f, 0x58, + 0x7a, 0x6d, 0x62, 0xb8, 0x96, 0x4d, 0x0c, 0xd1, 0x7a, 0x89, 0x35, 0xca, 0x6f, 0xe4, 0xe5, 0xdf, + 0x10, 0x85, 0x53, 0x9a, 0x4c, 0x8a, 0x7e, 0x53, 0xe3, 0x57, 0x1a, 0x74, 0x72, 0x07, 0x22, 0xbb, + 0x50, 0x95, 0x7c, 0xa6, 0x65, 0xda, 0x53, 0xb4, 0x98, 0x3a, 0xb3, 0x04, 0x90, 0xf7, 0xa1, 0xc1, + 0x54, 0x0d, 0xa7, 0x2e, 0x79, 0x2d, 0x57, 0xda, 0x29, 0x7c, 0x0c, 0x23, 0xdf, 0x06, 0x3d, 0x36, + 0x5d, 0xae, 0x7e, 0x8f, 0x2d, 0xad, 0x94, 0x12, 0xa0, 0xf1, 0x10, 0x9a, 0xa9, 0xed, 0xc9, 0xff, + 0x81, 0x3e, 0xa7, 0x2b, 0x55, 0x84, 0xcb, 0xf2, 0xad, 0x31, 0xa7, 0x2b, 0xac, 0xbf, 0xc9, 0x0d, + 0xa8, 0x0b, 0xe1, 0x84, 0x4a, 0xc3, 0x97, 0xcd, 0xda, 0x9c, 0xae, 0x7e, 0x40, 0x43, 0x63, 0x0f, + 0xda, 0xd9, 0x63, 0x45, 0xd0, 0x88, 0x10, 0x25, 0xf4, 0x68, 0xc2, 0x8c, 0x7b, 0xd0, 0xc9, 0x9d, + 0x86, 0x18, 0xd0, 0xf2, 0x17, 0x23, 0xeb, 0x25, 0x3b, 0xb7, 0xf0, 0xb8, 0xe8, 0x26, 0xba, 0xd9, + 0xf4, 0x17, 0xa3, 0x8f, 0xd9, 0xf9, 0x33, 0x31, 0x65, 0x3c, 0x85, 0x76, 0xb6, 0x3c, 0x16, 0x29, + 0x33, 0xf0, 0x16, 0xae, 0x8d, 0xeb, 0x57, 0x4d, 0x39, 0x10, 0x1d, 0xf6, 0xd2, 0x93, 0x9e, 0x91, + 0xae, 0x87, 0xcf, 0x3c, 0xce, 0x52, 0x45, 0xb5, 0xc4, 0x18, 0x0e, 0x54, 0xf1, 0x37, 0x17, 0xbf, + 0x9f, 0xc0, 0x45, 0x14, 0x2c, 0xbe, 0xc9, 0x13, 0x00, 0xca, 0x79, 0xe0, 0x8c, 0x16, 0xc9, 0x72, + 0xed, 0x81, 0x7c, 0xf6, 0x18, 0x7c, 0x7c, 0x76, 0x4a, 0x9d, 0x60, 0x78, 0x4b, 0xf9, 0xca, 0x56, + 0x82, 0x4c, 0xf9, 0x4b, 0x4a, 0xdf, 0xf8, 0x45, 0x15, 0x6a, 0xb2, 0x2d, 0x20, 0x83, 0x6c, 0xd3, + 0x29, 0x56, 0x55, 0x87, 0x94, 0xb3, 0xea, 0x8c, 0x31, 0xe3, 0xbf, 0x95, 0xef, 0xdc, 0x86, 0xcd, + 0x8b, 0x57, 0x3b, 0x75, 0x64, 0xcb, 0x93, 0x47, 0x49, 0x1b, 0xb7, 0xae, 0xcb, 0x89, 0x7a, 0xc6, + 0xca, 0xd7, 0xee, 0x19, 0x6f, 0x40, 0xdd, 0x5d, 0xcc, 0x2d, 0xbe, 0x0a, 0x55, 0xb6, 0xa9, 0xb9, + 0x8b, 0xf9, 0xb3, 0x15, 0x7a, 0x09, 0xf7, 0x38, 0x9d, 0xa1, 0x48, 0xe6, 0x9a, 0x06, 0x4e, 0x08, + 0xe1, 0x21, 0xb4, 0x52, 0x45, 0x85, 0x63, 0xab, 0xe2, 0xb4, 0x9d, 0x76, 0xf6, 0x93, 0x47, 0xea, + 0x96, 0xcd, 0xb8, 0xc8, 0x38, 0xb1, 0xc9, 0x6e, 0xb6, 0x45, 0xc2, 0x5a, 0xa4, 0x81, 0x21, 0x95, + 0xea, 0x82, 0x44, 0x25, 0x22, 0x0e, 0x20, 0x82, 0x4c, 0x42, 0x74, 0x84, 0x34, 0xc4, 0x04, 0x0a, + 0xdf, 0x86, 0x4e, 0x42, 0xe7, 0x12, 0x02, 0x72, 0x95, 0x64, 0x1a, 0x81, 0xef, 0xc1, 0x96, 0xcb, + 0x56, 0xdc, 0xca, 0xa3, 0x9b, 0x88, 0x26, 0x42, 0x76, 0x96, 0xd5, 0xf8, 0x26, 0xb4, 0x93, 0x54, + 0x84, 0xd8, 0x4d, 0xd9, 0xa8, 0xc6, 0xb3, 0x08, 0xbb, 0x09, 0x8d, 0xb8, 0x98, 0x6a, 0x21, 0xa0, + 0x4e, 0x65, 0x0d, 0x15, 0x97, 0x67, 0x01, 0x0b, 0x17, 0x33, 0xae, 0x16, 0x69, 0x23, 0x06, 0xcb, + 0x33, 0x53, 0xce, 0x23, 0xf6, 0x0e, 0xb4, 0xa2, 0xe8, 0x96, 0xb8, 0x0e, 0xe2, 0x36, 0xa3, 0x49, + 0x04, 0xed, 0x41, 0xd7, 0x0f, 0x3c, 0xdf, 0x0b, 0x59, 0x60, 0x51, 0xdb, 0x0e, 0x58, 0x18, 0xf6, + 0xba, 0x72, 0xbd, 0x68, 0xfe, 0x48, 0x4e, 0x1b, 0xef, 0x43, 0x3d, 0xaa, 0x12, 0xb7, 0xa0, 0x3a, + 0x8c, 0x33, 0x51, 0xc5, 0x94, 0x03, 0xc1, 0x43, 0x47, 0xbe, 0xaf, 0xde, 0x3a, 0xc4, 0xa7, 0xf1, + 0x33, 0xa8, 0xab, 0x1f, 0xac, 0xb0, 0x03, 0xfe, 0x1e, 0x6c, 0xfa, 0x34, 0x10, 0xd7, 0x48, 0xf7, + 0xc1, 0x51, 0x1f, 0x72, 0x4a, 0x03, 0xfe, 0x94, 0xf1, 0x4c, 0x3b, 0xdc, 0x44, 0xbc, 0x9c, 0x32, + 0xee, 0x43, 0x2b, 0x83, 0x11, 0xc7, 0x42, 0x3f, 0x8a, 0x82, 0x1a, 0x07, 0xf1, 0xce, 0xa5, 0x64, + 0x67, 0xe3, 0x01, 0xe8, 0xf1, 0x6f, 0x23, 0xca, 0xe5, 0xe8, 0xea, 0x9a, 0x32, 0xb7, 0x1c, 0x62, + 0x8b, 0xef, 0x7d, 0xca, 0x02, 0x15, 0x13, 0x72, 0x60, 0x3c, 0x4f, 0x25, 0x21, 0xc9, 0x0a, 0xe4, + 0x2e, 0xd4, 0x55, 0x12, 0x52, 0x51, 0x19, 0x35, 0xf3, 0xa7, 0x98, 0x85, 0xa2, 0x66, 0x5e, 0xe6, + 0xa4, 0x64, 0xd9, 0x52, 0x7a, 0xd9, 0x19, 0x34, 0xa2, 0x44, 0x93, 0xcd, 0xc6, 0x72, 0xc5, 0x6e, + 0x3e, 0x1b, 0xab, 0x45, 0x13, 0xa0, 0xf0, 0x8e, 0xd0, 0x99, 0xb8, 0xcc, 0xb6, 0x92, 0x10, 0xc2, + 0x3d, 0x1a, 0x66, 0x47, 0x0a, 0x9e, 0x44, 0xf1, 0x62, 0xbc, 0x07, 0x35, 0x79, 0xb6, 0xc2, 0xf4, + 0x55, 0x44, 0x49, 0xbf, 0xd7, 0xa0, 0x11, 0xe5, 0xe9, 0x42, 0xa5, 0xcc, 0xa1, 0x4b, 0x5f, 0xf5, + 0xd0, 0xff, 0xfd, 0xc4, 0x73, 0x17, 0x88, 0xcc, 0x2f, 0x4b, 0x8f, 0x3b, 0xee, 0xc4, 0x92, 0xb6, + 0x96, 0x39, 0xa8, 0x8b, 0x92, 0x33, 0x14, 0x9c, 0x8a, 0xf9, 0x83, 0xcf, 0xaa, 0xd0, 0x39, 0x1a, + 0x3e, 0x3c, 0x39, 0xf2, 0xfd, 0x99, 0x33, 0xa6, 0xd8, 0x95, 0xec, 0x43, 0x05, 0x1b, 0xb3, 0x82, + 0x87, 0xe5, 0x7e, 0xd1, 0x0b, 0x01, 0x39, 0x80, 0x2a, 0xf6, 0x67, 0xa4, 0xe8, 0x7d, 0xb9, 0x5f, + 0xf8, 0x50, 0x20, 0x36, 0x91, 0x1d, 0xdc, 0xe5, 0x67, 0xe6, 0x7e, 0xd1, 0x6b, 0x01, 0xf9, 0x08, + 0xf4, 0xa4, 0x71, 0x5a, 0xf7, 0xd8, 0xdc, 0x5f, 0xfb, 0x6e, 0x20, 0xf4, 0x93, 0xe2, 0x72, 0xdd, + 0x9b, 0x69, 0x7f, 0x6d, 0x83, 0x4d, 0x0e, 0xa1, 0x1e, 0x95, 0xe5, 0xc5, 0xcf, 0xc1, 0xfd, 0x35, + 0x3d, 0xbd, 0x30, 0x8f, 0xec, 0x85, 0x8a, 0xde, 0xac, 0xfb, 0x85, 0x0f, 0x0f, 0xe4, 0x1e, 0xd4, + 0x54, 0x7d, 0x54, 0xf8, 0x24, 0xdc, 0x2f, 0xee, 0xcc, 0xc5, 0x25, 0x93, 0x6e, 0x70, 0xdd, 0xbb, + 0x7a, 0x7f, 0xed, 0x0b, 0x09, 0x39, 0x02, 0x48, 0xb5, 0x34, 0x6b, 0x1f, 0xcc, 0xfb, 0xeb, 0x5f, + 0x3e, 0xc8, 0x03, 0x68, 0x24, 0xaf, 0x59, 0xc5, 0x4f, 0xe0, 0xfd, 0x75, 0x8f, 0x11, 0xc3, 0x5b, + 0xff, 0xf8, 0xd3, 0xb6, 0xf6, 0xeb, 0x8b, 0x6d, 0xed, 0x8b, 0x8b, 0x6d, 0xed, 0xcb, 0x8b, 0x6d, + 0xed, 0x77, 0x17, 0xdb, 0xda, 0x1f, 0x2f, 0xb6, 0xb5, 0xdf, 0xfc, 0x79, 0x5b, 0x1b, 0xd5, 0xd0, + 0xfd, 0x3f, 0xf8, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0x3e, 0x33, 0xc0, 0x3b, 0xf2, 0x19, 0x00, + 0x00, } diff --git a/abci/types/types.proto b/abci/types/types.proto index 4fdf575c6..8a2da5b47 100644 --- a/abci/types/types.proto +++ b/abci/types/types.proto @@ -165,7 +165,7 @@ message ResponseQuery { } message ResponseBeginBlock { - repeated common.KVPair tags = 1 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; + repeated Event events = 1 [(gogoproto.nullable)=false, (gogoproto.jsontag)="events,omitempty"]; } message ResponseCheckTx { @@ -175,7 +175,7 @@ message ResponseCheckTx { string info = 4; // nondeterministic int64 gas_wanted = 5; int64 gas_used = 6; - repeated common.KVPair tags = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; + repeated Event events = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="events,omitempty"]; string codespace = 8; } @@ -186,14 +186,14 @@ message ResponseDeliverTx { string info = 4; // nondeterministic int64 gas_wanted = 5; int64 gas_used = 6; - repeated common.KVPair tags = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; + repeated Event events = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="events,omitempty"]; string codespace = 8; } message ResponseEndBlock { repeated ValidatorUpdate validator_updates = 1 [(gogoproto.nullable)=false]; ConsensusParams consensus_param_updates = 2; - repeated common.KVPair tags = 3 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; + repeated Event events = 3 [(gogoproto.nullable)=false, (gogoproto.jsontag)="events,omitempty"]; } message ResponseCommit { @@ -236,6 +236,11 @@ message LastCommitInfo { repeated VoteInfo votes = 2 [(gogoproto.nullable)=false]; } +message Event { + string type = 1; + repeated common.KVPair attributes = 2 [(gogoproto.nullable)=false, (gogoproto.jsontag)="attributes,omitempty"]; +} + //---------------------------------------- // Blockchain Types diff --git a/abci/types/typespb_test.go b/abci/types/typespb_test.go index a4c0a3f82..523561532 100644 --- a/abci/types/typespb_test.go +++ b/abci/types/typespb_test.go @@ -1703,6 +1703,62 @@ func TestLastCommitInfoMarshalTo(t *testing.T) { } } +func TestEventProto(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedEvent(popr, false) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &Event{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + littlefuzz := make([]byte, len(dAtA)) + copy(littlefuzz, dAtA) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } + if len(littlefuzz) > 0 { + fuzzamount := 100 + for i := 0; i < fuzzamount; i++ { + littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256)) + littlefuzz = append(littlefuzz, byte(popr.Intn(256))) + } + // shouldn't panic + _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg) + } +} + +func TestEventMarshalTo(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedEvent(popr, false) + size := p.Size() + dAtA := make([]byte, size) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + _, err := p.MarshalTo(dAtA) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &Event{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestHeaderProto(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -2747,6 +2803,24 @@ func TestLastCommitInfoJSON(t *testing.T) { t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) } } +func TestEventJSON(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedEvent(popr, true) + marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{} + jsondata, err := marshaler.MarshalToString(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &Event{} + err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) + } +} func TestHeaderJSON(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -3749,6 +3823,34 @@ func TestLastCommitInfoProtoCompactText(t *testing.T) { } } +func TestEventProtoText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedEvent(popr, true) + dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p) + msg := &Event{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + +func TestEventProtoCompactText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedEvent(popr, true) + dAtA := github_com_gogo_protobuf_proto.CompactTextString(p) + msg := &Event{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestHeaderProtoText(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -4661,6 +4763,28 @@ func TestLastCommitInfoSize(t *testing.T) { } } +func TestEventSize(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedEvent(popr, true) + size2 := github_com_gogo_protobuf_proto.Size(p) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + size := p.Size() + if len(dAtA) != size { + t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA)) + } + if size2 != size { + t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2) + } + size3 := github_com_gogo_protobuf_proto.Size(p) + if size3 != size { + t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3) + } +} + func TestHeaderSize(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 52ef9ba64..1a4a87d08 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -292,7 +292,7 @@ func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { } func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { - return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} + return abci.ResponseDeliverTx{Events: []abci.Event{}} } func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { diff --git a/docs/spec/abci/abci.md b/docs/spec/abci/abci.md index c65d96ec1..6dc00b5b9 100644 --- a/docs/spec/abci/abci.md +++ b/docs/spec/abci/abci.md @@ -38,20 +38,58 @@ Finally, `Query`, `CheckTx`, and `DeliverTx` include a `Codespace string`, whose intended use is to disambiguate `Code` values returned by different domains of the application. The `Codespace` is a namespace for the `Code`. -## Tags +## Events Some methods (`CheckTx, BeginBlock, DeliverTx, EndBlock`) -include a `Tags` field in their `Response*`. Each tag is key-value pair denoting -something about what happened during the methods execution. +include an `Events` field in their `Response*`. Each event contains a type and a +list of attributes, which are key-value pairs denoting something about what happened +during the method's execution. -Tags can be used to index transactions and blocks according to what happened -during their execution. Note that the set of tags returned for a block from +Events can be used to index transactions and blocks according to what happened +during their execution. Note that the set of events returned for a block from `BeginBlock` and `EndBlock` are merged. In case both methods return the same tag, only the value defined in `EndBlock` is used. -Keys and values in tags must be UTF-8 encoded strings (e.g. -"account.owner": "Bob", "balance": "100.0", -"time": "2018-01-02T12:30:00Z") +Each event has a `type` which is meant to categorize the event for a particular +`Response*` or tx. A `Response*` or tx may contain multiple events with duplicate +`type` values, where each distinct entry is meant to categorize attributes for a +particular event. Every key and value in an event's attributes must be UTF-8 +encoded strings along with the even type itself. + +Example: + +```go + abci.ResponseDeliverTx{ + // ... + Events: []abci.Event{ + { + Type: "validator.provisions", + Attributes: cmn.KVPairs{ + cmn.KVPair{Key: []byte("address"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, + }, + }, + { + Type: "validator.provisions", + Attributes: cmn.KVPairs{ + cmn.KVPair{Key: []byte("address"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, + }, + }, + { + Type: "validator.slashed", + Attributes: cmn.KVPairs{ + cmn.KVPair{Key: []byte("address"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("reason"), Value: []byte("...")}, + }, + }, + // ... + }, +} +``` ## Determinism diff --git a/libs/pubsub/example_test.go b/libs/pubsub/example_test.go index a43696266..34bb2a88f 100644 --- a/libs/pubsub/example_test.go +++ b/libs/pubsub/example_test.go @@ -21,7 +21,7 @@ func TestExample(t *testing.T) { ctx := context.Background() subscription, err := s.Subscribe(ctx, "example-client", query.MustParse("abci.account.name='John'")) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Tombstone", map[string]string{"abci.account.name": "John"}) + err = s.PublishWithEvents(ctx, "Tombstone", map[string][]string{"abci.account.name": {"John"}}) require.NoError(t, err) assertReceive(t, "Tombstone", subscription.Out()) } diff --git a/libs/pubsub/pubsub.go b/libs/pubsub/pubsub.go index f78dac1ba..cb7b8d5bb 100644 --- a/libs/pubsub/pubsub.go +++ b/libs/pubsub/pubsub.go @@ -26,7 +26,7 @@ // for { // select { // case msg <- subscription.Out(): -// // handle msg.Data() and msg.Tags() +// // handle msg.Data() and msg.Events() // case <-subscription.Cancelled(): // return subscription.Err() // } @@ -61,9 +61,14 @@ var ( ErrAlreadySubscribed = errors.New("already subscribed") ) -// Query defines an interface for a query to be used for subscribing. +// Query defines an interface for a query to be used for subscribing. A query +// matches against a map of events. Each key in this map is a composite of the +// even type and an attribute key (e.g. "{eventType}.{eventAttrKey}") and the +// values are the event values that are contained under that relationship. This +// allows event types to repeat themselves with the same set of keys and +// different values. type Query interface { - Matches(tags map[string]string) bool + Matches(events map[string][]string) bool String() string } @@ -76,12 +81,12 @@ type cmd struct { clientID string // publish - msg interface{} - tags map[string]string + msg interface{} + events map[string][]string } // Server allows clients to subscribe/unsubscribe for messages, publishing -// messages with or without tags, and manages internal state. +// messages with or without events, and manages internal state. type Server struct { cmn.BaseService @@ -258,15 +263,15 @@ func (s *Server) NumClientSubscriptions(clientID string) int { // Publish publishes the given message. An error will be returned to the caller // if the context is canceled. func (s *Server) Publish(ctx context.Context, msg interface{}) error { - return s.PublishWithTags(ctx, msg, make(map[string]string)) + return s.PublishWithEvents(ctx, msg, make(map[string][]string)) } -// PublishWithTags publishes the given message with the set of tags. The set is -// matched with clients queries. If there is a match, the message is sent to +// PublishWithEvents publishes the given message with the set of events. The set +// is matched with clients queries. If there is a match, the message is sent to // the client. -func (s *Server) PublishWithTags(ctx context.Context, msg interface{}, tags map[string]string) error { +func (s *Server) PublishWithEvents(ctx context.Context, msg interface{}, events map[string][]string) error { select { - case s.cmds <- cmd{op: pub, msg: msg, tags: tags}: + case s.cmds <- cmd{op: pub, msg: msg, events: events}: return nil case <-ctx.Done(): return ctx.Err() @@ -325,7 +330,7 @@ loop: case sub: state.add(cmd.clientID, cmd.query, cmd.subscription) case pub: - state.send(cmd.msg, cmd.tags) + state.send(cmd.msg, cmd.events) } } } @@ -392,18 +397,18 @@ func (state *state) removeAll(reason error) { } } -func (state *state) send(msg interface{}, tags map[string]string) { +func (state *state) send(msg interface{}, events map[string][]string) { for qStr, clientSubscriptions := range state.subscriptions { q := state.queries[qStr].q - if q.Matches(tags) { + if q.Matches(events) { for clientID, subscription := range clientSubscriptions { if cap(subscription.out) == 0 { // block on unbuffered channel - subscription.out <- Message{msg, tags} + subscription.out <- NewMessage(msg, events) } else { // don't block on buffered channels select { - case subscription.out <- Message{msg, tags}: + case subscription.out <- NewMessage(msg, events): default: state.remove(clientID, qStr, ErrOutOfCapacity) } diff --git a/libs/pubsub/pubsub_test.go b/libs/pubsub/pubsub_test.go index 74af431f8..d5f61dc07 100644 --- a/libs/pubsub/pubsub_test.go +++ b/libs/pubsub/pubsub_test.go @@ -136,24 +136,75 @@ func TestDifferentClients(t *testing.T) { ctx := context.Background() subscription1, err := s.Subscribe(ctx, "client-1", query.MustParse("tm.events.type='NewBlock'")) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Iceman", map[string]string{"tm.events.type": "NewBlock"}) + err = s.PublishWithEvents(ctx, "Iceman", map[string][]string{"tm.events.type": {"NewBlock"}}) require.NoError(t, err) assertReceive(t, "Iceman", subscription1.Out()) subscription2, err := s.Subscribe(ctx, "client-2", query.MustParse("tm.events.type='NewBlock' AND abci.account.name='Igor'")) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Ultimo", map[string]string{"tm.events.type": "NewBlock", "abci.account.name": "Igor"}) + err = s.PublishWithEvents(ctx, "Ultimo", map[string][]string{"tm.events.type": {"NewBlock"}, "abci.account.name": {"Igor"}}) require.NoError(t, err) assertReceive(t, "Ultimo", subscription1.Out()) assertReceive(t, "Ultimo", subscription2.Out()) subscription3, err := s.Subscribe(ctx, "client-3", query.MustParse("tm.events.type='NewRoundStep' AND abci.account.name='Igor' AND abci.invoice.number = 10")) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Valeria Richards", map[string]string{"tm.events.type": "NewRoundStep"}) + err = s.PublishWithEvents(ctx, "Valeria Richards", map[string][]string{"tm.events.type": {"NewRoundStep"}}) require.NoError(t, err) assert.Zero(t, len(subscription3.Out())) } +func TestSubscribeDuplicateKeys(t *testing.T) { + ctx := context.Background() + s := pubsub.NewServer() + s.SetLogger(log.TestingLogger()) + require.NoError(t, s.Start()) + defer s.Stop() + + testCases := []struct { + query string + expected interface{} + }{ + { + "withdraw.rewards='17'", + "Iceman", + }, + { + "withdraw.rewards='22'", + "Iceman", + }, + { + "withdraw.rewards='1' AND withdraw.rewards='22'", + "Iceman", + }, + { + "withdraw.rewards='100'", + nil, + }, + } + + for i, tc := range testCases { + sub, err := s.Subscribe(ctx, fmt.Sprintf("client-%d", i), query.MustParse(tc.query)) + require.NoError(t, err) + + err = s.PublishWithEvents( + ctx, + "Iceman", + map[string][]string{ + "transfer.sender": {"foo", "bar", "baz"}, + "withdraw.rewards": {"1", "17", "22"}, + }, + ) + require.NoError(t, err) + + if tc.expected != nil { + assertReceive(t, tc.expected, sub.Out()) + } else { + require.Zero(t, len(sub.Out())) + } + } +} + func TestClientSubscribesTwice(t *testing.T) { s := pubsub.NewServer() s.SetLogger(log.TestingLogger()) @@ -165,7 +216,7 @@ func TestClientSubscribesTwice(t *testing.T) { subscription1, err := s.Subscribe(ctx, clientID, q) require.NoError(t, err) - err = s.PublishWithTags(ctx, "Goblin Queen", map[string]string{"tm.events.type": "NewBlock"}) + err = s.PublishWithEvents(ctx, "Goblin Queen", map[string][]string{"tm.events.type": {"NewBlock"}}) require.NoError(t, err) assertReceive(t, "Goblin Queen", subscription1.Out()) @@ -173,7 +224,7 @@ func TestClientSubscribesTwice(t *testing.T) { require.Error(t, err) require.Nil(t, subscription2) - err = s.PublishWithTags(ctx, "Spider-Man", map[string]string{"tm.events.type": "NewBlock"}) + err = s.PublishWithEvents(ctx, "Spider-Man", map[string][]string{"tm.events.type": {"NewBlock"}}) require.NoError(t, err) assertReceive(t, "Spider-Man", subscription1.Out()) } @@ -312,7 +363,7 @@ func benchmarkNClients(n int, b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - s.PublishWithTags(ctx, "Gamora", map[string]string{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": string(i)}) + s.PublishWithEvents(ctx, "Gamora", map[string][]string{"abci.Account.Owner": {"Ivan"}, "abci.Invoices.Number": {string(i)}}) } } @@ -343,7 +394,7 @@ func benchmarkNClientsOneQuery(n int, b *testing.B) { b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - s.PublishWithTags(ctx, "Gamora", map[string]string{"abci.Account.Owner": "Ivan", "abci.Invoices.Number": "1"}) + s.PublishWithEvents(ctx, "Gamora", map[string][]string{"abci.Account.Owner": {"Ivan"}, "abci.Invoices.Number": {"1"}}) } } diff --git a/libs/pubsub/query/empty.go b/libs/pubsub/query/empty.go index 83271f047..2d7642adc 100644 --- a/libs/pubsub/query/empty.go +++ b/libs/pubsub/query/empty.go @@ -5,7 +5,7 @@ type Empty struct { } // Matches always returns true. -func (Empty) Matches(tags map[string]string) bool { +func (Empty) Matches(tags map[string][]string) bool { return true } diff --git a/libs/pubsub/query/empty_test.go b/libs/pubsub/query/empty_test.go index 141fb9515..3fcd2d728 100644 --- a/libs/pubsub/query/empty_test.go +++ b/libs/pubsub/query/empty_test.go @@ -10,8 +10,8 @@ import ( func TestEmptyQueryMatchesAnything(t *testing.T) { q := query.Empty{} - assert.True(t, q.Matches(map[string]string{})) - assert.True(t, q.Matches(map[string]string{"Asher": "Roth"})) - assert.True(t, q.Matches(map[string]string{"Route": "66"})) - assert.True(t, q.Matches(map[string]string{"Route": "66", "Billy": "Blue"})) + assert.True(t, q.Matches(map[string][]string{})) + assert.True(t, q.Matches(map[string][]string{"Asher": {"Roth"}})) + assert.True(t, q.Matches(map[string][]string{"Route": {"66"}})) + assert.True(t, q.Matches(map[string][]string{"Route": {"66"}, "Billy": {"Blue"}})) } diff --git a/libs/pubsub/query/query.go b/libs/pubsub/query/query.go index 189110a3e..80dbfc056 100644 --- a/libs/pubsub/query/query.go +++ b/libs/pubsub/query/query.go @@ -148,12 +148,14 @@ func (q *Query) Conditions() []Condition { return conditions } -// Matches returns true if the query matches the given set of tags, false otherwise. +// 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. // -// For example, query "name=John" matches tags = {"name": "John"}. More -// examples could be found in parser_test.go and query_test.go. -func (q *Query) Matches(tags map[string]string) bool { - if len(tags) == 0 { +// 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(events map[string][]string) bool { + if len(events) == 0 { return false } @@ -162,7 +164,8 @@ func (q *Query) Matches(tags map[string]string) bool { var tag string var op Operator - // tokens must be in the following order: tag ("tx.gas") -> operator ("=") -> operand ("7") + // tokens must be in the following order: + // tag ("tx.gas") -> operator ("=") -> operand ("7") for _, token := range q.parser.Tokens() { switch token.pegRule { @@ -188,7 +191,7 @@ func (q *Query) Matches(tags map[string]string) bool { // see if the triplet (tag, operator, operand) matches any tag // "tx.gas", "=", "7", { "tx.gas": 7, "tx.ID": "4AE393495334" } - if !match(tag, op, reflect.ValueOf(valueWithoutSingleQuotes), tags) { + if !match(tag, op, reflect.ValueOf(valueWithoutSingleQuotes), events) { return false } case rulenumber: @@ -198,7 +201,7 @@ func (q *Query) Matches(tags map[string]string) bool { if err != nil { panic(fmt.Sprintf("got %v while trying to parse %s as float64 (should never happen if the grammar is correct)", err, number)) } - if !match(tag, op, reflect.ValueOf(value), tags) { + if !match(tag, op, reflect.ValueOf(value), events) { return false } } else { @@ -206,7 +209,7 @@ func (q *Query) Matches(tags map[string]string) bool { if err != nil { panic(fmt.Sprintf("got %v while trying to parse %s as int64 (should never happen if the grammar is correct)", err, number)) } - if !match(tag, op, reflect.ValueOf(value), tags) { + if !match(tag, op, reflect.ValueOf(value), events) { return false } } @@ -215,7 +218,7 @@ func (q *Query) Matches(tags map[string]string) bool { if err != nil { panic(fmt.Sprintf("got %v while trying to parse %s as time.Time / RFC3339 (should never happen if the grammar is correct)", err, buffer[begin:end])) } - if !match(tag, op, reflect.ValueOf(value), tags) { + if !match(tag, op, reflect.ValueOf(value), events) { return false } case ruledate: @@ -223,7 +226,7 @@ func (q *Query) Matches(tags map[string]string) bool { if err != nil { panic(fmt.Sprintf("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])) } - if !match(tag, op, reflect.ValueOf(value), tags) { + if !match(tag, op, reflect.ValueOf(value), events) { return false } } @@ -232,34 +235,53 @@ func (q *Query) Matches(tags map[string]string) bool { return true } -// match returns true if the given triplet (tag, operator, operand) matches any tag. +// match returns true if the given triplet (tag, operator, operand) matches any +// value in an event for that key. // -// First, it looks up the tag in tags and if it finds one, tries to compare the -// value from it to the operand using the operator. +// 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, "tx.ID": "4AE393495334" } -func match(tag string, op Operator, operand reflect.Value, tags map[string]string) bool { +// "tx.gas", "=", "7", {"tx": [{"gas": 7, "ID": "4AE393495334"}]} +func match(tag string, op Operator, operand reflect.Value, events map[string][]string) bool { // look up the tag from the query in tags - value, ok := tags[tag] + values, ok := events[tag] if !ok { return false } + + for _, value := range values { + // return true if any value in the set of the event's values matches + if matchValue(value, op, operand) { + return true + } + } + + return false +} + +// matchValue will attempt to match a string value against an operation an +// operand. A boolean is returned representing the match result. It will panic +// if an error occurs or if the operand is invalid. +func matchValue(value string, op Operator, operand reflect.Value) bool { switch operand.Kind() { case reflect.Struct: // time operandAsTime := operand.Interface().(time.Time) + // try our best to convert value from tags 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 { - panic(fmt.Sprintf("Failed to convert value %v from tag to time.Time: %v", value, err)) + panic(fmt.Sprintf("failed to convert value %v from tag to time.Time: %v", value, err)) } + switch op { case OpLessEqual: return v.Before(operandAsTime) || v.Equal(operandAsTime) @@ -272,14 +294,17 @@ func match(tag string, op Operator, operand reflect.Value, tags map[string]strin case OpEqual: return v.Equal(operandAsTime) } + case reflect.Float64: operandFloat64 := operand.Interface().(float64) var v float64 + // try our best to convert value from tags to float64 v, err := strconv.ParseFloat(value, 64) if err != nil { - panic(fmt.Sprintf("Failed to convert value %v from tag to float64: %v", value, err)) + panic(fmt.Sprintf("failed to convert value %v from tag to float64: %v", value, err)) } + switch op { case OpLessEqual: return v <= operandFloat64 @@ -292,6 +317,7 @@ func match(tag string, op Operator, operand reflect.Value, tags map[string]strin case OpEqual: return v == operandFloat64 } + case reflect.Int64: operandInt := operand.Interface().(int64) var v int64 @@ -299,7 +325,7 @@ func match(tag string, op Operator, operand reflect.Value, tags map[string]strin if strings.ContainsAny(value, ".") { v1, err := strconv.ParseFloat(value, 64) if err != nil { - panic(fmt.Sprintf("Failed to convert value %v from tag to float64: %v", value, err)) + panic(fmt.Sprintf("failed to convert value %v from tag to float64: %v", value, err)) } v = int64(v1) } else { @@ -307,7 +333,7 @@ func match(tag string, op Operator, operand reflect.Value, tags map[string]strin // try our best to convert value from tags to int64 v, err = strconv.ParseInt(value, 10, 64) if err != nil { - panic(fmt.Sprintf("Failed to convert value %v from tag to int64: %v", value, err)) + panic(fmt.Sprintf("failed to convert value %v from tag to int64: %v", value, err)) } } switch op { @@ -322,6 +348,7 @@ func match(tag string, op Operator, operand reflect.Value, tags map[string]strin case OpEqual: return v == operandInt } + case reflect.String: switch op { case OpEqual: @@ -329,8 +356,9 @@ func match(tag string, op Operator, operand reflect.Value, tags map[string]strin case OpContains: return strings.Contains(value, operand.String()) } + default: - panic(fmt.Sprintf("Unknown kind of operand %v", operand.Kind())) + panic(fmt.Sprintf("unknown kind of operand %v", operand.Kind())) } return false diff --git a/libs/pubsub/query/query_test.go b/libs/pubsub/query/query_test.go index a3d83b259..10dc3d221 100644 --- a/libs/pubsub/query/query_test.go +++ b/libs/pubsub/query/query_test.go @@ -19,30 +19,40 @@ func TestMatches(t *testing.T) { testCases := []struct { s string - tags map[string]string + events map[string][]string err bool matches bool }{ - {"tm.events.type='NewBlock'", map[string]string{"tm.events.type": "NewBlock"}, false, true}, - - {"tx.gas > 7", map[string]string{"tx.gas": "8"}, false, true}, - {"tx.gas > 7 AND tx.gas < 9", map[string]string{"tx.gas": "8"}, false, true}, - {"body.weight >= 3.5", map[string]string{"body.weight": "3.5"}, false, true}, - {"account.balance < 1000.0", map[string]string{"account.balance": "900"}, false, true}, - {"apples.kg <= 4", map[string]string{"apples.kg": "4.0"}, false, true}, - {"body.weight >= 4.5", map[string]string{"body.weight": fmt.Sprintf("%v", float32(4.5))}, false, true}, - {"oranges.kg < 4 AND watermellons.kg > 10", map[string]string{"oranges.kg": "3", "watermellons.kg": "12"}, false, true}, - {"peaches.kg < 4", map[string]string{"peaches.kg": "5"}, false, false}, - - {"tx.date > DATE 2017-01-01", map[string]string{"tx.date": time.Now().Format(query.DateLayout)}, false, true}, - {"tx.date = DATE 2017-01-01", map[string]string{"tx.date": txDate}, false, true}, - {"tx.date = DATE 2018-01-01", map[string]string{"tx.date": txDate}, false, false}, - - {"tx.time >= TIME 2013-05-03T14:45:00Z", map[string]string{"tx.time": time.Now().Format(query.TimeLayout)}, false, true}, - {"tx.time = TIME 2013-05-03T14:45:00Z", map[string]string{"tx.time": txTime}, false, false}, - - {"abci.owner.name CONTAINS 'Igor'", map[string]string{"abci.owner.name": "Igor,Ivan"}, false, true}, - {"abci.owner.name CONTAINS 'Igor'", map[string]string{"abci.owner.name": "Pavel,Ivan"}, false, false}, + {"tm.events.type='NewBlock'", map[string][]string{"tm.events.type": {"NewBlock"}}, false, true}, + + {"tx.gas > 7", map[string][]string{"tx.gas": {"8"}}, false, true}, + {"tx.gas > 7 AND tx.gas < 9", map[string][]string{"tx.gas": {"8"}}, false, true}, + {"body.weight >= 3.5", map[string][]string{"body.weight": {"3.5"}}, false, true}, + {"account.balance < 1000.0", map[string][]string{"account.balance": {"900"}}, false, true}, + {"apples.kg <= 4", map[string][]string{"apples.kg": {"4.0"}}, false, true}, + {"body.weight >= 4.5", map[string][]string{"body.weight": {fmt.Sprintf("%v", float32(4.5))}}, false, true}, + {"oranges.kg < 4 AND watermellons.kg > 10", map[string][]string{"oranges.kg": {"3"}, "watermellons.kg": {"12"}}, false, true}, + {"peaches.kg < 4", map[string][]string{"peaches.kg": {"5"}}, false, false}, + + {"tx.date > DATE 2017-01-01", map[string][]string{"tx.date": {time.Now().Format(query.DateLayout)}}, false, true}, + {"tx.date = DATE 2017-01-01", map[string][]string{"tx.date": {txDate}}, false, true}, + {"tx.date = DATE 2018-01-01", map[string][]string{"tx.date": {txDate}}, false, false}, + + {"tx.time >= TIME 2013-05-03T14:45:00Z", map[string][]string{"tx.time": {time.Now().Format(query.TimeLayout)}}, false, true}, + {"tx.time = TIME 2013-05-03T14:45:00Z", map[string][]string{"tx.time": {txTime}}, false, false}, + + {"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Igor,Ivan"}}, false, true}, + {"abci.owner.name CONTAINS 'Igor'", map[string][]string{"abci.owner.name": {"Pavel,Ivan"}}, false, false}, + + {"abci.owner.name = 'Igor'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, false, true}, + {"abci.owner.name = 'Ivan'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, false, true}, + {"abci.owner.name = 'Ivan' AND abci.owner.name = 'Igor'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, false, true}, + {"abci.owner.name = 'Ivan' AND abci.owner.name = 'John'", map[string][]string{"abci.owner.name": {"Igor", "Ivan"}}, false, false}, + + {"tm.events.type='NewBlock'", map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, false, true}, + {"app.name = 'fuzzed'", map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, false, true}, + {"tm.events.type='NewBlock' AND app.name = 'fuzzed'", map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, false, true}, + {"tm.events.type='NewHeader' AND app.name = 'fuzzed'", map[string][]string{"tm.events.type": {"NewBlock"}, "app.name": {"fuzzed"}}, false, false}, } for _, tc := range testCases { @@ -51,10 +61,12 @@ func TestMatches(t *testing.T) { require.Nil(t, err) } + require.NotNil(t, q, "Query '%s' should not be nil", tc.s) + if tc.matches { - assert.True(t, q.Matches(tc.tags), "Query '%s' should match %v", tc.s, tc.tags) + assert.True(t, q.Matches(tc.events), "Query '%s' should match %v", tc.s, tc.events) } else { - assert.False(t, q.Matches(tc.tags), "Query '%s' should not match %v", tc.s, tc.tags) + assert.False(t, q.Matches(tc.events), "Query '%s' should not match %v", tc.s, tc.events) } } } diff --git a/libs/pubsub/subscription.go b/libs/pubsub/subscription.go index 2660439f5..40c97c9ee 100644 --- a/libs/pubsub/subscription.go +++ b/libs/pubsub/subscription.go @@ -70,12 +70,12 @@ func (s *Subscription) cancel(err error) { // Message glues data and tags together. type Message struct { - data interface{} - tags map[string]string + data interface{} + events map[string][]string } -func NewMessage(data interface{}, tags map[string]string) Message { - return Message{data, tags} +func NewMessage(data interface{}, events map[string][]string) Message { + return Message{data, events} } // Data returns an original data published. @@ -83,7 +83,7 @@ func (msg Message) Data() interface{} { return msg.data } -// Tags returns tags, which matched the client's query. -func (msg Message) Tags() map[string]string { - return msg.tags +// Events returns events, which matched the client's query. +func (msg Message) Events() map[string][]string { + return msg.events } diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index d57ced311..161f44fdf 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -182,7 +182,7 @@ func (c *Local) eventsRoutine(sub types.Subscription, subscriber string, q tmpub for { select { case msg := <-sub.Out(): - result := ctypes.ResultEvent{Query: q.String(), Data: msg.Data(), Tags: msg.Tags()} + result := ctypes.ResultEvent{Query: q.String(), Data: msg.Data(), Events: msg.Events()} if cap(outc) == 0 { outc <- result } else { diff --git a/rpc/core/events.go b/rpc/core/events.go index 4b05e5ce1..acb90b46f 100644 --- a/rpc/core/events.go +++ b/rpc/core/events.go @@ -22,26 +22,83 @@ import ( // string (escaped with single quotes), number, date or time. // // Examples: -// tm.event = 'NewBlock' # new blocks -// tm.event = 'CompleteProposal' # node got a complete proposal +// tm.event = 'NewBlock' # new blocks +// tm.event = 'CompleteProposal' # node got a complete proposal // tm.event = 'Tx' AND tx.hash = 'XYZ' # single transaction -// tm.event = 'Tx' AND tx.height = 5 # all txs of the fifth block -// tx.height = 5 # all txs of the fifth block +// tm.event = 'Tx' AND tx.height = 5 # all txs of the fifth block +// tx.height = 5 # all txs of the fifth block // // Tendermint provides a few predefined keys: tm.event, tx.hash and tx.height. -// Note for transactions, you can define additional keys by providing tags with +// Note for transactions, you can define additional keys by providing events with // DeliverTx response. // -// DeliverTx{ -// Tags: []*KVPair{ -// "agent.name": "K", -// } -// } +// import ( +// abci "github.com/tendermint/tendermint/abci/types" +// "github.com/tendermint/tendermint/libs/pubsub/query" +// ) // -// tm.event = 'Tx' AND agent.name = 'K' -// tm.event = 'Tx' AND account.created_at >= TIME 2013-05-03T14:45:00Z -// tm.event = 'Tx' AND contract.sign_date = DATE 2017-01-01 -// tm.event = 'Tx' AND account.owner CONTAINS 'Igor' +// abci.ResponseDeliverTx{ +// Events: []abci.Event{ +// { +// Type: "rewards.withdraw", +// Attributes: cmn.KVPairs{ +// cmn.KVPair{Key: []byte("address"), Value: []byte("AddrA")}, +// cmn.KVPair{Key: []byte("source"), Value: []byte("SrcX")}, +// cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, +// cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, +// }, +// }, +// { +// Type: "rewards.withdraw", +// Attributes: cmn.KVPairs{ +// cmn.KVPair{Key: []byte("address"), Value: []byte("AddrB")}, +// cmn.KVPair{Key: []byte("source"), Value: []byte("SrcY")}, +// cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, +// cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, +// }, +// }, +// { +// Type: "transfer", +// Attributes: cmn.KVPairs{ +// cmn.KVPair{Key: []byte("sender"), Value: []byte("AddrC")}, +// cmn.KVPair{Key: []byte("recipient"), Value: []byte("AddrD")}, +// cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, +// }, +// }, +// }, +// } +// +// All events are indexed by a composite key of the form {eventType}.{evenAttrKey}. +// In the above examples, the following keys would be indexed: +// - rewards.withdraw.address +// - rewards.withdraw.source +// - rewards.withdraw.amount +// - rewards.withdraw.balance +// - transfer.sender +// - transfer.recipient +// - transfer.amount +// +// Multiple event types with duplicate keys are allowed and are meant to +// categorize unique and distinct events. In the above example, all events +// indexed under the key `rewards.withdraw.address` will have the following +// values stored and queryable: +// +// - AddrA +// - AddrB +// +// To create a query for txs where address AddrA withdrew rewards: +// query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA'") +// +// To create a query for txs where address AddrA withdrew rewards from source Y: +// query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA' AND rewards.withdraw.source = 'Y'") +// +// To create a query for txs where AddrA transferred funds: +// query.MustParse("tm.event = 'Tx' AND transfer.sender = 'AddrA'") +// +// The following queries would return no results: +// query.MustParse("tm.event = 'Tx' AND transfer.sender = 'AddrZ'") +// query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrZ'") +// query.MustParse("tm.event = 'Tx' AND rewards.withdraw.source = 'W'") // // See list of all possible events here // https://godoc.org/github.com/tendermint/tendermint/types#pkg-constants @@ -106,8 +163,10 @@ func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, er if err != nil { return nil, errors.Wrap(err, "failed to parse query") } + subCtx, cancel := context.WithTimeout(ctx.Context(), SubscribeTimeout) defer cancel() + sub, err := eventBus.Subscribe(subCtx, addr, q) if err != nil { return nil, err @@ -117,7 +176,7 @@ func Subscribe(ctx *rpctypes.Context, query string) (*ctypes.ResultSubscribe, er for { select { case msg := <-sub.Out(): - resultEvent := &ctypes.ResultEvent{Query: query, Data: msg.Data(), Tags: msg.Tags()} + resultEvent := &ctypes.ResultEvent{Query: query, Data: msg.Data(), Events: msg.Events()} ctx.WSConn.TryWriteRPCResponse( rpctypes.NewRPCSuccessResponse( ctx.WSConn.Codec(), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 74457b38a..f1ae16a39 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -205,7 +205,7 @@ type ( // Event data from a subscription type ResultEvent struct { - Query string `json:"query"` - Data types.TMEventData `json:"data"` - Tags map[string]string `json:"tags"` + Query string `json:"query"` + Data types.TMEventData `json:"data"` + Events map[string][]string `json:"events"` } diff --git a/state/execution_test.go b/state/execution_test.go index 465b6b0be..80b442e3d 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -13,7 +13,6 @@ import ( abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/secp256k1" - cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" @@ -455,7 +454,7 @@ func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { } func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { - return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} + return abci.ResponseDeliverTx{Events: []abci.Event{}} } func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { diff --git a/state/state_test.go b/state/state_test.go index eddbe255b..c7600cc38 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -93,8 +93,8 @@ func TestABCIResponsesSaveLoad1(t *testing.T) { // Build mock responses. block := makeBlock(state, 2) abciResponses := NewABCIResponses(block) - abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Tags: nil} - abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Tags: nil} + abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Events: nil} + abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Events: nil} abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{ types.TM2PB.NewValidatorUpdate(ed25519.GenPrivKey().PubKey(), 10), }} @@ -134,11 +134,13 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { 2: { []*abci.ResponseDeliverTx{ {Code: 383}, - {Data: []byte("Gotcha!"), - Tags: []cmn.KVPair{ - {Key: []byte("a"), Value: []byte("1")}, - {Key: []byte("build"), Value: []byte("stuff")}, - }}, + { + Data: []byte("Gotcha!"), + Events: []abci.Event{ + {Type: "type1", Attributes: []cmn.KVPair{{Key: []byte("a"), Value: []byte("1")}}}, + {Type: "type2", Attributes: []cmn.KVPair{{Key: []byte("build"), Value: []byte("stuff")}}}, + }, + }, }, types.ABCIResults{ {383, nil}, diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index 84208b8c1..053d26a71 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -10,9 +10,9 @@ import ( "time" "github.com/pkg/errors" + cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" @@ -75,7 +75,10 @@ func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) { return txResult, nil } -// AddBatch indexes a batch of transactions using the given list of tags. +// AddBatch indexes a batch of transactions using the given list of events. Each +// key that indexed from the tx's events is a composite of the event type and +// the respective attribute's key delimited by a "." (eg. "account.number"). +// Any event with an empty type is not indexed. func (txi *TxIndex) AddBatch(b *txindex.Batch) error { storeBatch := txi.store.NewBatch() defer storeBatch.Close() @@ -83,12 +86,8 @@ func (txi *TxIndex) AddBatch(b *txindex.Batch) error { for _, result := range b.Ops { hash := result.Tx.Hash() - // index tx by tags - for _, tag := range result.Result.Tags { - if txi.indexAllTags || cmn.StringInSlice(string(tag.Key), txi.tagsToIndex) { - storeBatch.Set(keyForTag(tag, result), hash) - } - } + // index tx by events + txi.indexEvents(result, hash, storeBatch) // index tx by height if txi.indexAllTags || cmn.StringInSlice(types.TxHeightKey, txi.tagsToIndex) { @@ -107,19 +106,18 @@ func (txi *TxIndex) AddBatch(b *txindex.Batch) error { return nil } -// Index indexes a single transaction using the given list of tags. +// Index indexes a single transaction using the given list of events. Each key +// that indexed from the tx's events is a composite of the event type and the +// respective attribute's key delimited by a "." (eg. "account.number"). +// Any event with an empty type is not indexed. func (txi *TxIndex) Index(result *types.TxResult) error { b := txi.store.NewBatch() defer b.Close() hash := result.Tx.Hash() - // index tx by tags - for _, tag := range result.Result.Tags { - if txi.indexAllTags || cmn.StringInSlice(string(tag.Key), txi.tagsToIndex) { - b.Set(keyForTag(tag, result), hash) - } - } + // index tx by events + txi.indexEvents(result, hash, b) // index tx by height if txi.indexAllTags || cmn.StringInSlice(types.TxHeightKey, txi.tagsToIndex) { @@ -131,12 +129,33 @@ func (txi *TxIndex) Index(result *types.TxResult) error { if err != nil { return err } - b.Set(hash, rawBytes) + b.Set(hash, rawBytes) b.Write() + return nil } +func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.SetDeleter) { + for _, event := range result.Result.Events { + // only index events with a non-empty type + if len(event.Type) == 0 { + continue + } + + for _, attr := range event.Attributes { + if len(attr.Key) == 0 { + continue + } + + compositeTag := fmt.Sprintf("%s.%s", event.Type, string(attr.Key)) + if txi.indexAllTags || cmn.StringInSlice(compositeTag, txi.tagsToIndex) { + store.Set(keyForEvent(compositeTag, attr.Value, result), hash) + } + } + } +} + // Search performs a search using the given query. It breaks the query into // conditions (like "tx.height > 5"). For each condition, it queries the DB // index. One special use cases here: (1) if "tx.hash" is found, it returns tx @@ -343,7 +362,7 @@ func (txi *TxIndex) match(c query.Condition, startKeyBz []byte) (hashes [][]byte } } else if c.Op == query.OpContains { // XXX: startKey does not apply here. - // For example, if startKey = "account.owner/an/" and search query = "accoutn.owner CONTAINS an" + // 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 := dbm.IteratePrefix(txi.store, startKey(c.Tag)) defer it.Close() @@ -420,10 +439,10 @@ func extractValueFromKey(key []byte) string { return parts[1] } -func keyForTag(tag cmn.KVPair, result *types.TxResult) []byte { +func keyForEvent(key string, value []byte, result *types.TxResult) []byte { return []byte(fmt.Sprintf("%s/%s/%d/%d", - tag.Key, - tag.Value, + key, + value, result.Height, result.Index, )) diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index b726a423c..cacfaad01 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -21,7 +21,15 @@ func TestTxIndex(t *testing.T) { indexer := NewTxIndex(db.NewMemDB()) tx := types.Tx("HELLO WORLD") - txResult := &types.TxResult{1, 0, tx, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: nil}} + txResult := &types.TxResult{ + Height: 1, + Index: 0, + Tx: tx, + Result: abci.ResponseDeliverTx{ + Data: []byte{0}, + Code: abci.CodeTypeOK, Log: "", Events: nil, + }, + } hash := tx.Hash() batch := txindex.NewBatch(1) @@ -36,7 +44,15 @@ func TestTxIndex(t *testing.T) { assert.Equal(t, txResult, loadedTxResult) tx2 := types.Tx("BYE BYE WORLD") - txResult2 := &types.TxResult{1, 0, tx2, abci.ResponseDeliverTx{Data: []byte{0}, Code: abci.CodeTypeOK, Log: "", Tags: nil}} + txResult2 := &types.TxResult{ + Height: 1, + Index: 0, + Tx: tx2, + Result: abci.ResponseDeliverTx{ + Data: []byte{0}, + Code: abci.CodeTypeOK, Log: "", Events: nil, + }, + } hash2 := tx2.Hash() err = indexer.Index(txResult2) @@ -51,10 +67,10 @@ func TestTxSearch(t *testing.T) { allowedTags := []string{"account.number", "account.owner", "account.date"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags)) - txResult := txResultWithTags([]cmn.KVPair{ - {Key: []byte("account.number"), Value: []byte("1")}, - {Key: []byte("account.owner"), Value: []byte("Ivan")}, - {Key: []byte("not_allowed"), Value: []byte("Vlad")}, + txResult := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("1")}}}, + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("owner"), Value: []byte("Ivan")}}}, + {Type: "", Attributes: []cmn.KVPair{{Key: []byte("not_allowed"), Value: []byte("Vlad")}}}, }) hash := txResult.Tx.Hash() @@ -108,13 +124,82 @@ func TestTxSearch(t *testing.T) { } } +func TestTxSearchDeprecatedIndexing(t *testing.T) { + allowedTags := []string{"account.number", "sender"} + indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags)) + + // index tx using events indexing (composite key) + txResult1 := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("1")}}}, + }) + hash1 := txResult1.Tx.Hash() + + err := indexer.Index(txResult1) + require.NoError(t, err) + + // index tx also using deprecated indexing (tag as key) + txResult2 := txResultWithEvents(nil) + txResult2.Tx = types.Tx("HELLO WORLD 2") + + hash2 := txResult2.Tx.Hash() + b := indexer.store.NewBatch() + + rawBytes, err := cdc.MarshalBinaryBare(txResult2) + require.NoError(t, err) + + depKey := []byte(fmt.Sprintf("%s/%s/%d/%d", + "sender", + "addr1", + txResult2.Height, + txResult2.Index, + )) + + b.Set(depKey, hash2) + b.Set(keyForHeight(txResult2), hash2) + b.Set(hash2, rawBytes) + b.Write() + + testCases := []struct { + q string + results []*types.TxResult + }{ + // search by hash + {fmt.Sprintf("tx.hash = '%X'", hash1), []*types.TxResult{txResult1}}, + // search by hash + {fmt.Sprintf("tx.hash = '%X'", hash2), []*types.TxResult{txResult2}}, + // search by exact match (one tag) + {"account.number = 1", []*types.TxResult{txResult1}}, + {"account.number >= 1 AND account.number <= 5", []*types.TxResult{txResult1}}, + // search by range (lower bound) + {"account.number >= 1", []*types.TxResult{txResult1}}, + // search by range (upper bound) + {"account.number <= 5", []*types.TxResult{txResult1}}, + // search using not allowed tag + {"not_allowed = 'boom'", []*types.TxResult{}}, + // search for not existing tx result + {"account.number >= 2 AND account.number <= 5", []*types.TxResult{}}, + // search using not existing tag + {"account.date >= TIME 2013-05-03T14:45:00Z", []*types.TxResult{}}, + // search by deprecated tag + {"sender = 'addr1'", []*types.TxResult{txResult2}}, + } + + for _, tc := range testCases { + t.Run(tc.q, func(t *testing.T) { + results, err := indexer.Search(query.MustParse(tc.q)) + require.NoError(t, err) + require.Equal(t, results, tc.results) + }) + } +} + func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { allowedTags := []string{"account.number"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags)) - txResult := txResultWithTags([]cmn.KVPair{ - {Key: []byte("account.number"), Value: []byte("1")}, - {Key: []byte("account.number"), Value: []byte("2")}, + txResult := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("1")}}}, + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("2")}}}, }) err := indexer.Index(txResult) @@ -132,9 +217,10 @@ func TestTxSearchMultipleTxs(t *testing.T) { indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags)) // indexed first, but bigger height (to test the order of transactions) - txResult := txResultWithTags([]cmn.KVPair{ - {Key: []byte("account.number"), Value: []byte("1")}, + txResult := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("1")}}}, }) + txResult.Tx = types.Tx("Bob's account") txResult.Height = 2 txResult.Index = 1 @@ -142,8 +228,8 @@ func TestTxSearchMultipleTxs(t *testing.T) { require.NoError(t, err) // indexed second, but smaller height (to test the order of transactions) - txResult2 := txResultWithTags([]cmn.KVPair{ - {Key: []byte("account.number"), Value: []byte("2")}, + txResult2 := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("2")}}}, }) txResult2.Tx = types.Tx("Alice's account") txResult2.Height = 1 @@ -153,8 +239,8 @@ func TestTxSearchMultipleTxs(t *testing.T) { require.NoError(t, err) // indexed third (to test the order of transactions) - txResult3 := txResultWithTags([]cmn.KVPair{ - {Key: []byte("account.number"), Value: []byte("3")}, + txResult3 := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("3")}}}, }) txResult3.Tx = types.Tx("Jack's account") txResult3.Height = 1 @@ -164,8 +250,8 @@ func TestTxSearchMultipleTxs(t *testing.T) { // indexed fourth (to test we don't include txs with similar tags) // https://github.com/tendermint/tendermint/issues/2908 - txResult4 := txResultWithTags([]cmn.KVPair{ - {Key: []byte("account.number.id"), Value: []byte("1")}, + txResult4 := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number.id"), Value: []byte("1")}}}, }) txResult4.Tx = types.Tx("Mike's account") txResult4.Height = 2 @@ -183,9 +269,9 @@ func TestTxSearchMultipleTxs(t *testing.T) { func TestIndexAllTags(t *testing.T) { indexer := NewTxIndex(db.NewMemDB(), IndexAllTags()) - txResult := txResultWithTags([]cmn.KVPair{ - {Key: []byte("account.owner"), Value: []byte("Ivan")}, - {Key: []byte("account.number"), Value: []byte("1")}, + txResult := txResultWithEvents([]abci.Event{ + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("owner"), Value: []byte("Ivan")}}}, + {Type: "account", Attributes: []cmn.KVPair{{Key: []byte("number"), Value: []byte("1")}}}, }) err := indexer.Index(txResult) @@ -202,17 +288,17 @@ func TestIndexAllTags(t *testing.T) { assert.Equal(t, []*types.TxResult{txResult}, results) } -func txResultWithTags(tags []cmn.KVPair) *types.TxResult { +func txResultWithEvents(events []abci.Event) *types.TxResult { tx := types.Tx("HELLO WORLD") return &types.TxResult{ Height: 1, Index: 0, Tx: tx, Result: abci.ResponseDeliverTx{ - Data: []byte{0}, - Code: abci.CodeTypeOK, - Log: "", - Tags: tags, + Data: []byte{0}, + Code: abci.CodeTypeOK, + Log: "", + Events: events, }, } } @@ -236,10 +322,10 @@ func benchmarkTxIndex(txsCount int64, b *testing.B) { Index: txIndex, Tx: tx, Result: abci.ResponseDeliverTx{ - Data: []byte{0}, - Code: abci.CodeTypeOK, - Log: "", - Tags: []cmn.KVPair{}, + Data: []byte{0}, + Code: abci.CodeTypeOK, + Log: "", + Events: []abci.Event{}, }, } if err := batch.Add(txResult); err != nil { diff --git a/types/event_bus.go b/types/event_bus.go index da959090f..9c50d6430 100644 --- a/types/event_bus.go +++ b/types/event_bus.go @@ -4,6 +4,7 @@ import ( "context" "fmt" + "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" tmpubsub "github.com/tendermint/tendermint/libs/pubsub" @@ -90,20 +91,32 @@ func (b *EventBus) UnsubscribeAll(ctx context.Context, subscriber string) error func (b *EventBus) Publish(eventType string, eventData TMEventData) error { // no explicit deadline for publishing events ctx := context.Background() - b.pubsub.PublishWithTags(ctx, eventData, map[string]string{EventTypeKey: eventType}) - return nil -} - -func (b *EventBus) validateAndStringifyTags(tags []cmn.KVPair, logger log.Logger) map[string]string { - result := make(map[string]string) - for _, tag := range tags { - // basic validation - if len(tag.Key) == 0 { - logger.Debug("Got tag with an empty key (skipping)", "tag", tag) + return b.pubsub.PublishWithEvents(ctx, eventData, map[string][]string{EventTypeKey: {eventType}}) +} + +// validateAndStringifyEvents takes a slice of event objects and creates a +// map of stringified events where each key is composed of the event +// type and each of the event's attributes keys in the form of +// "{event.Type}.{attribute.Key}" and the value is each attribute's value. +func (b *EventBus) validateAndStringifyEvents(events []types.Event, logger log.Logger) map[string][]string { + result := make(map[string][]string) + for _, event := range events { + if len(event.Type) == 0 { + logger.Debug("Got an event with an empty type (skipping)", "event", event) continue } - result[string(tag.Key)] = string(tag.Value) + + for _, attr := range event.Attributes { + if len(attr.Key) == 0 { + logger.Debug("Got an event attribute with an empty key(skipping)", "event", event) + continue + } + + compositeTag := fmt.Sprintf("%s.%s", event.Type, string(attr.Key)) + result[compositeTag] = append(result[compositeTag], string(attr.Value)) + } } + return result } @@ -111,14 +124,13 @@ func (b *EventBus) PublishEventNewBlock(data EventDataNewBlock) error { // no explicit deadline for publishing events ctx := context.Background() - resultTags := append(data.ResultBeginBlock.Tags, data.ResultEndBlock.Tags...) - tags := b.validateAndStringifyTags(resultTags, b.Logger.With("block", data.Block.StringShort())) + resultEvents := append(data.ResultBeginBlock.Events, data.ResultEndBlock.Events...) + events := b.validateAndStringifyEvents(resultEvents, b.Logger.With("block", data.Block.StringShort())) - // add predefined tags - logIfTagExists(EventTypeKey, tags, b.Logger) - tags[EventTypeKey] = EventNewBlock + // add predefined new block event + events[EventTypeKey] = append(events[EventTypeKey], EventNewBlock) - b.pubsub.PublishWithTags(ctx, data, tags) + _ = b.pubsub.PublishWithEvents(ctx, data, events) return nil } @@ -126,16 +138,14 @@ func (b *EventBus) PublishEventNewBlockHeader(data EventDataNewBlockHeader) erro // no explicit deadline for publishing events ctx := context.Background() - resultTags := append(data.ResultBeginBlock.Tags, data.ResultEndBlock.Tags...) + resultTags := append(data.ResultBeginBlock.Events, data.ResultEndBlock.Events...) // TODO: Create StringShort method for Header and use it in logger. - tags := b.validateAndStringifyTags(resultTags, b.Logger.With("header", data.Header)) + events := b.validateAndStringifyEvents(resultTags, b.Logger.With("header", data.Header)) - // add predefined tags - logIfTagExists(EventTypeKey, tags, b.Logger) - tags[EventTypeKey] = EventNewBlockHeader + // add predefined new block header event + events[EventTypeKey] = append(events[EventTypeKey], EventNewBlockHeader) - b.pubsub.PublishWithTags(ctx, data, tags) - return nil + return b.pubsub.PublishWithEvents(ctx, data, events) } func (b *EventBus) PublishEventVote(data EventDataVote) error { @@ -153,19 +163,14 @@ func (b *EventBus) PublishEventTx(data EventDataTx) error { // no explicit deadline for publishing events ctx := context.Background() - tags := b.validateAndStringifyTags(data.Result.Tags, b.Logger.With("tx", data.Tx)) + events := b.validateAndStringifyEvents(data.Result.Events, b.Logger.With("tx", data.Tx)) // add predefined tags - logIfTagExists(EventTypeKey, tags, b.Logger) - tags[EventTypeKey] = EventTx - - logIfTagExists(TxHashKey, tags, b.Logger) - tags[TxHashKey] = fmt.Sprintf("%X", data.Tx.Hash()) - - logIfTagExists(TxHeightKey, tags, b.Logger) - tags[TxHeightKey] = fmt.Sprintf("%d", data.Height) + events[EventTypeKey] = append(events[EventTypeKey], EventTx) + events[TxHashKey] = append(events[TxHashKey], fmt.Sprintf("%X", data.Tx.Hash())) + events[TxHeightKey] = append(events[TxHeightKey], fmt.Sprintf("%d", data.Height)) - b.pubsub.PublishWithTags(ctx, data, tags) + _ = b.pubsub.PublishWithEvents(ctx, data, events) return nil } @@ -209,12 +214,6 @@ func (b *EventBus) PublishEventValidatorSetUpdates(data EventDataValidatorSetUpd return b.Publish(EventValidatorSetUpdates, data) } -func logIfTagExists(tag string, tags map[string]string, logger log.Logger) { - if value, ok := tags[tag]; ok { - logger.Error("Found predefined tag (value will be overwritten)", "tag", tag, "value", value) - } -} - //----------------------------------------------------------------------------- type NopEventBus struct{} diff --git a/types/event_bus_test.go b/types/event_bus_test.go index 508b423a6..45590217f 100644 --- a/types/event_bus_test.go +++ b/types/event_bus_test.go @@ -22,10 +22,15 @@ func TestEventBusPublishEventTx(t *testing.T) { defer eventBus.Stop() tx := Tx("foo") - result := abci.ResponseDeliverTx{Data: []byte("bar"), Tags: []cmn.KVPair{{Key: []byte("baz"), Value: []byte("1")}}} + result := abci.ResponseDeliverTx{ + Data: []byte("bar"), + Events: []abci.Event{ + {Type: "testType", Attributes: []cmn.KVPair{{Key: []byte("baz"), Value: []byte("1")}}}, + }, + } // PublishEventTx adds all these 3 tags, so the query below should work - query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X' AND baz=1", tx.Hash()) + query := fmt.Sprintf("tm.event='Tx' AND tx.height=1 AND tx.hash='%X' AND testType.baz=1", tx.Hash()) txsSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query)) require.NoError(t, err) @@ -62,11 +67,19 @@ func TestEventBusPublishEventNewBlock(t *testing.T) { defer eventBus.Stop() block := MakeBlock(0, []Tx{}, nil, []Evidence{}) - resultBeginBlock := abci.ResponseBeginBlock{Tags: []cmn.KVPair{{Key: []byte("baz"), Value: []byte("1")}}} - resultEndBlock := abci.ResponseEndBlock{Tags: []cmn.KVPair{{Key: []byte("foz"), Value: []byte("2")}}} + resultBeginBlock := abci.ResponseBeginBlock{ + Events: []abci.Event{ + {Type: "testType", Attributes: []cmn.KVPair{{Key: []byte("baz"), Value: []byte("1")}}}, + }, + } + resultEndBlock := abci.ResponseEndBlock{ + Events: []abci.Event{ + {Type: "testType", Attributes: []cmn.KVPair{{Key: []byte("foz"), Value: []byte("2")}}}, + }, + } // PublishEventNewBlock adds the tm.event tag, so the query below should work - query := "tm.event='NewBlock' AND baz=1 AND foz=2" + query := "tm.event='NewBlock' AND testType.baz=1 AND testType.foz=2" blocksSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query)) require.NoError(t, err) @@ -94,6 +107,106 @@ func TestEventBusPublishEventNewBlock(t *testing.T) { } } +func TestEventBusPublishEventTxDuplicateKeys(t *testing.T) { + eventBus := NewEventBus() + err := eventBus.Start() + require.NoError(t, err) + defer eventBus.Stop() + + tx := Tx("foo") + result := abci.ResponseDeliverTx{ + Data: []byte("bar"), + Events: []abci.Event{ + { + Type: "transfer", + Attributes: []cmn.KVPair{ + {Key: []byte("sender"), Value: []byte("foo")}, + {Key: []byte("recipient"), Value: []byte("bar")}, + {Key: []byte("amount"), Value: []byte("5")}, + }, + }, + { + Type: "transfer", + Attributes: []cmn.KVPair{ + {Key: []byte("sender"), Value: []byte("baz")}, + {Key: []byte("recipient"), Value: []byte("cat")}, + {Key: []byte("amount"), Value: []byte("13")}, + }, + }, + { + Type: "withdraw.rewards", + Attributes: []cmn.KVPair{ + {Key: []byte("address"), Value: []byte("bar")}, + {Key: []byte("source"), Value: []byte("iceman")}, + {Key: []byte("amount"), Value: []byte("33")}, + }, + }, + }, + } + + testCases := []struct { + query string + expectResults bool + }{ + { + "tm.event='Tx' AND tx.height=1 AND transfer.sender='DoesNotExist'", + false, + }, + { + "tm.event='Tx' AND tx.height=1 AND transfer.sender='foo'", + true, + }, + { + "tm.event='Tx' AND tx.height=1 AND transfer.sender='baz'", + true, + }, + { + "tm.event='Tx' AND tx.height=1 AND transfer.sender='foo' AND transfer.sender='baz'", + true, + }, + { + "tm.event='Tx' AND tx.height=1 AND transfer.sender='foo' AND transfer.sender='DoesNotExist'", + false, + }, + } + + for i, tc := range testCases { + sub, err := eventBus.Subscribe(context.Background(), fmt.Sprintf("client-%d", i), tmquery.MustParse(tc.query)) + require.NoError(t, err) + + done := make(chan struct{}) + + go func() { + msg := <-sub.Out() + data := msg.Data().(EventDataTx) + assert.Equal(t, int64(1), data.Height) + assert.Equal(t, uint32(0), data.Index) + assert.Equal(t, tx, data.Tx) + assert.Equal(t, result, data.Result) + close(done) + }() + + err = eventBus.PublishEventTx(EventDataTx{TxResult{ + Height: 1, + Index: 0, + Tx: tx, + Result: result, + }}) + assert.NoError(t, err) + + select { + case <-done: + if !tc.expectResults { + require.Fail(t, "unexpected transaction result(s) from subscription") + } + case <-time.After(1 * time.Second): + if tc.expectResults { + require.Fail(t, "failed to receive a transaction after 1 second") + } + } + } +} + func TestEventBusPublishEventNewBlockHeader(t *testing.T) { eventBus := NewEventBus() err := eventBus.Start() @@ -101,11 +214,19 @@ func TestEventBusPublishEventNewBlockHeader(t *testing.T) { defer eventBus.Stop() block := MakeBlock(0, []Tx{}, nil, []Evidence{}) - resultBeginBlock := abci.ResponseBeginBlock{Tags: []cmn.KVPair{{Key: []byte("baz"), Value: []byte("1")}}} - resultEndBlock := abci.ResponseEndBlock{Tags: []cmn.KVPair{{Key: []byte("foz"), Value: []byte("2")}}} + resultBeginBlock := abci.ResponseBeginBlock{ + Events: []abci.Event{ + {Type: "testType", Attributes: []cmn.KVPair{{Key: []byte("baz"), Value: []byte("1")}}}, + }, + } + resultEndBlock := abci.ResponseEndBlock{ + Events: []abci.Event{ + {Type: "testType", Attributes: []cmn.KVPair{{Key: []byte("foz"), Value: []byte("2")}}}, + }, + } // PublishEventNewBlockHeader adds the tm.event tag, so the query below should work - query := "tm.event='NewBlockHeader' AND baz=1 AND foz=2" + query := "tm.event='NewBlockHeader' AND testType.baz=1 AND testType.foz=2" headersSub, err := eventBus.Subscribe(context.Background(), "test", tmquery.MustParse(query)) require.NoError(t, err) From 9010ff5f964447e89edd921866bbec9efb3cf753 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 12 Jun 2019 15:25:47 +0200 Subject: [PATCH 062/211] types: do not ignore errors returned by PublishWithEvents (#3722) Follow up to #3643 * update changelog * do not ignore errors returned by PublishWithEvents --- CHANGELOG_PENDING.md | 36 +++++++++++++++++------------------- types/event_bus.go | 6 ++---- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index c23dd76b9..b1b09e20c 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -4,26 +4,25 @@ ### BREAKING CHANGES: -- \#3613 Switch from golang/dep to Go 1.11 Modules to resolve dependencies: - - it is recommended to switch to Go Modules if your project has tendermint - as a dependency - - read more on Modules here: https://github.com/golang/go/wiki/Modules - * CLI/RPC/Config - * [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` -> - `results.deliver_tx`). See docs for details. + - [cli] \#3613 Switch from golang/dep to Go Modules to resolve dependencies: + It is recommended to switch to Go Modules if your project has tendermint as + a dependency. Read more on Modules here: + https://github.com/golang/go/wiki/Modules + - [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` + -> `results.deliver_tx`). See docs for details. * Apps - * [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, - and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` - contains a `type` and a list of `attributes` (list of key-value pairs) allowing - for inclusion of multiple distinct events in each response. + - [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, + and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` + contains a `type` and a list of `attributes` (list of key-value pairs) + allowing for inclusion of multiple distinct events in each response. * Go API - * [libs/db] Removed deprecated `LevelDBBackend` const - * If you have `db_backend` set to `leveldb` in your config file, please + - [libs/db] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed deprecated `LevelDBBackend` const + If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. -- [p2p] \#3521 Remove NewNetAddressStringWithOptionalID + - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID * Blockchain Protocol @@ -32,10 +31,9 @@ ### FEATURES: ### IMPROVEMENTS: -- [p2p] \#3666 Add per channel telemtry to improve reactor observability - -* [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. +- [p2p] \#3666 Add per channel telemetry to improve reactor observability +- [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) ### BUG FIXES: -- [libs/db] Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) -- [libs/db] Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) +- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) diff --git a/types/event_bus.go b/types/event_bus.go index 9c50d6430..b91340c72 100644 --- a/types/event_bus.go +++ b/types/event_bus.go @@ -130,8 +130,7 @@ func (b *EventBus) PublishEventNewBlock(data EventDataNewBlock) error { // add predefined new block event events[EventTypeKey] = append(events[EventTypeKey], EventNewBlock) - _ = b.pubsub.PublishWithEvents(ctx, data, events) - return nil + return b.pubsub.PublishWithEvents(ctx, data, events) } func (b *EventBus) PublishEventNewBlockHeader(data EventDataNewBlockHeader) error { @@ -170,8 +169,7 @@ func (b *EventBus) PublishEventTx(data EventDataTx) error { events[TxHashKey] = append(events[TxHashKey], fmt.Sprintf("%X", data.Tx.Hash())) events[TxHeightKey] = append(events[TxHeightKey], fmt.Sprintf("%d", data.Height)) - _ = b.pubsub.PublishWithEvents(ctx, data, events) - return nil + return b.pubsub.PublishWithEvents(ctx, data, events) } func (b *EventBus) PublishEventNewRoundStep(data EventDataRoundState) error { From 0e1c492d3ed1429afe87f9ea651753878821b341 Mon Sep 17 00:00:00 2001 From: Andy Nogueira <45477427+andynog@users.noreply.github.com> Date: Mon, 17 Jun 2019 06:51:12 -0400 Subject: [PATCH 063/211] docs: missing 'b' in python command (#3728) In Python 3 the command outlined in this doc `import codecs; codecs.decode("YWJjZA==", 'base64').decode('ascii')` throws an error: TypeError: decoding with 'base64' codec failed (TypeError: expected bytes-like object, not str), needs to add 'b' before the encoded string `import codecs; codecs.decode(b"YWJjZA==", 'base64').decode('ascii')` to make it work --- docs/app-dev/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/app-dev/getting-started.md b/docs/app-dev/getting-started.md index 9110761e2..eff70db68 100644 --- a/docs/app-dev/getting-started.md +++ b/docs/app-dev/getting-started.md @@ -137,7 +137,7 @@ The result should look like: Note the `value` in the result (`YWJjZA==`); this is the base64-encoding of the ASCII of `abcd`. You can verify this in a python 2 shell by running `"YWJjZA==".decode('base64')` or in python 3 shell by running -`import codecs; codecs.decode("YWJjZA==", 'base64').decode('ascii')`. +`import codecs; codecs.decode(b"YWJjZA==", 'base64').decode('ascii')`. Stay tuned for a future release that [makes this output more human-readable](https://github.com/tendermint/tendermint/issues/1794). From ed18ffdca3876ecc962bbfefe6f50d89fc2f1f88 Mon Sep 17 00:00:00 2001 From: Runchao Han Date: Mon, 17 Jun 2019 21:30:12 +1000 Subject: [PATCH 064/211] p2p: refactor Switch#OnStop (#3729) --- node/node.go | 1 - p2p/switch.go | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/node/node.go b/node/node.go index 46185847e..6f2cad1e5 100644 --- a/node/node.go +++ b/node/node.go @@ -714,7 +714,6 @@ func (n *Node) OnStop() { n.indexerService.Stop() // now stop the reactors - // TODO: gracefully disconnect from peers. n.sw.Stop() // stop mempool WAL diff --git a/p2p/switch.go b/p2p/switch.go index 31e0aa6e1..7e681d67c 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -221,11 +221,7 @@ func (sw *Switch) OnStart() error { func (sw *Switch) OnStop() { // Stop peers for _, p := range sw.peers.List() { - sw.transport.Cleanup(p) - p.Stop() - if sw.peers.Remove(p) { - sw.metrics.Peers.Add(float64(-1)) - } + sw.stopAndRemovePeer(p, nil) } // Stop reactors From 60827f75623b92eff132dc0eff5b49d2025c591e Mon Sep 17 00:00:00 2001 From: Hans Schoenburg Date: Wed, 19 Jun 2019 20:35:53 +0200 Subject: [PATCH 065/211] docs: fix some language issues and deprecated link (#3733) --- docs/.vuepress/config.js | 2 +- docs/app-dev/app-development.md | 6 +++--- docs/spec/abci/abci.md | 2 +- docs/spec/blockchain/encoding.md | 2 +- docs/spec/blockchain/state.md | 2 +- docs/tendermint-core/using-tendermint.md | 4 +++- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 0b54d2011..9adfc5953 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -44,7 +44,7 @@ module.exports = { "/app-dev/app-development", "/app-dev/subscribing-to-events-via-websocket", "/app-dev/indexing-transactions", - "/app-dev/abci-spec", + "/spec/abci/abci", "/app-dev/ecosystem" ] }, diff --git a/docs/app-dev/app-development.md b/docs/app-dev/app-development.md index d157ce378..1b4e26800 100644 --- a/docs/app-dev/app-development.md +++ b/docs/app-dev/app-development.md @@ -133,8 +133,8 @@ the mempool. If Tendermint is just started or the clients sent more than 100k transactions, old transactions may be sent to the application. So it is important CheckTx implements some logic to handle them. -There are cases where a transaction will (or may) become valid in some -future state, in which case you probably want to disable Tendermint's +If there are cases in your application where a transaction may become invalid in some +future state, you probably want to disable Tendermint's cache. You can do that by setting `[mempool] cache_size = 0` in the config. @@ -205,7 +205,7 @@ Once all processing of the block is complete, Tendermint sends the Commit request and blocks waiting for a response. While the mempool may run concurrently with block processing (the BeginBlock, DeliverTxs, and EndBlock), it is locked for the Commit request so that its state can be -safely reset during Commit. This means the app _MUST NOT_ do any +safely updated during Commit. This means the app _MUST NOT_ do any blocking communication with the mempool (ie. broadcast_tx) during Commit, or there will be deadlock. Note also that all remaining transactions in the mempool are replayed on the mempool connection diff --git a/docs/spec/abci/abci.md b/docs/spec/abci/abci.md index 6dc00b5b9..b7b2e09fe 100644 --- a/docs/spec/abci/abci.md +++ b/docs/spec/abci/abci.md @@ -141,7 +141,7 @@ on them. All other fields in the `Response*` must be strictly deterministic. ## Block Execution The first time a new blockchain is started, Tendermint calls -`InitChain`. From then on, the follow sequence of methods is executed for each +`InitChain`. From then on, the following sequence of methods is executed for each block: `BeginBlock, [DeliverTx], EndBlock, Commit` diff --git a/docs/spec/blockchain/encoding.md b/docs/spec/blockchain/encoding.md index bde580a14..14d0e786b 100644 --- a/docs/spec/blockchain/encoding.md +++ b/docs/spec/blockchain/encoding.md @@ -218,7 +218,7 @@ func MerkleRoot(items [][]byte) []byte{ case 0: return nil case 1: - return leafHash(leafs[0]) + return leafHash(items[0]) default: k := getSplitPoint(len(items)) left := MerkleRoot(items[:k]) diff --git a/docs/spec/blockchain/state.md b/docs/spec/blockchain/state.md index 3ab65e12b..15fc37761 100644 --- a/docs/spec/blockchain/state.md +++ b/docs/spec/blockchain/state.md @@ -59,7 +59,7 @@ type Validator struct { When hashing the Validator struct, the address is not included, because it is redundant with the pubkey. -The `state.Validators`, `state.LastValidators`, and `state.NextValidators`, must always by sorted by validator address, +The `state.Validators`, `state.LastValidators`, and `state.NextValidators`, must always be sorted by validator address, so that there is a canonical order for computing the MerkleRoot. We also define a `TotalVotingPower` function, to return the total voting power: diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 2ca8c9e92..05d481b2c 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -202,8 +202,10 @@ Note that raw hex cannot be used in `POST` transactions. ## Reset -**WARNING: UNSAFE** Only do this in development and only if you can +::: warning +**UNSAFE** Only do this in development and only if you can afford to lose all blockchain data! +::: To reset a blockchain, stop the node and run: From 1b5110e91ffa81eb4387e88d57473ebaaed57ec0 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 21 Jun 2019 09:30:32 +0400 Subject: [PATCH 066/211] node: fix a bug where `nil` is recorded as node's address (#3740) * node: fix a bug where `nil` is recorded as node's address Solution AddOurAddress when we know it sw.NetAddress is nil in createAddrBookAndSetOnSwitch it's set by n.transport.Listen function, which is called during start Fixes #3716 * use addr instead of n.sw.NetAddress * add both ExternalAddress and ListenAddress as our addresses --- CHANGELOG_PENDING.md | 1 + node/node.go | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index b1b09e20c..63cd05ae2 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -37,3 +37,4 @@ ### BUG FIXES: - [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) - [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [node] \#3716 Fix a bug where `nil` is recorded as node's address diff --git a/node/node.go b/node/node.go index 6f2cad1e5..85fef5ee7 100644 --- a/node/node.go +++ b/node/node.go @@ -441,17 +441,30 @@ func createSwitch(config *cfg.Config, } func createAddrBookAndSetOnSwitch(config *cfg.Config, sw *p2p.Switch, - p2pLogger log.Logger) pex.AddrBook { + p2pLogger log.Logger, nodeKey *p2p.NodeKey) (pex.AddrBook, error) { addrBook := pex.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) // Add ourselves to addrbook to prevent dialing ourselves - addrBook.AddOurAddress(sw.NetAddress()) + if config.P2P.ExternalAddress != "" { + addr, err := p2p.NewNetAddressString(p2p.IDAddressString(nodeKey.ID(), config.P2P.ExternalAddress)) + if err != nil { + return nil, errors.Wrap(err, "p2p.external_address is incorrect") + } + addrBook.AddOurAddress(addr) + } + if config.P2P.ListenAddress != "" { + addr, err := p2p.NewNetAddressString(p2p.IDAddressString(nodeKey.ID(), config.P2P.ListenAddress)) + if err != nil { + return nil, errors.Wrap(err, "p2p.laddr is incorrect") + } + addrBook.AddOurAddress(addr) + } sw.SetAddrBook(addrBook) - return addrBook + return addrBook, nil } func createPEXReactorAndAddToSwitch(addrBook pex.AddrBook, config *cfg.Config, @@ -594,7 +607,10 @@ func NewNode(config *cfg.Config, return nil, errors.Wrap(err, "could not add peers from persistent_peers field") } - addrBook := createAddrBookAndSetOnSwitch(config, sw, p2pLogger) + addrBook, err := createAddrBookAndSetOnSwitch(config, sw, p2pLogger, nodeKey) + if err != nil { + return nil, errors.Wrap(err, "could not create addrbook") + } // Optionally, start the pex reactor // From 9d5ba576eeb11a979bd7aab166edaa22e256fc62 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 21 Jun 2019 01:56:27 -0400 Subject: [PATCH 067/211] abci: Refactor ABCI CheckTx and DeliverTx signatures (#3735) * Refactor signature of Application.CheckTx * Refactor signature of Application.DeliverTx * Refactor example variable names for clarity and consistency * Rename method variables for consistency * Rename method variables for consistency * add a changelog entry * update docs --- CHANGELOG_PENDING.md | 1 + abci/client/local_client.go | 8 +- abci/example/counter/counter.go | 16 +-- abci/example/kvstore/kvstore.go | 8 +- abci/example/kvstore/kvstore_test.go | 7 +- abci/example/kvstore/persistent_kvstore.go | 12 +- abci/server/socket_server.go | 4 +- abci/types/application.go | 12 +- blockchain/reactor_test.go | 4 +- consensus/mempool_test.go | 10 +- consensus/replay.go | 2 +- docs/app-dev/app-development.md | 139 +++++++++++---------- docs/app-dev/indexing-transactions.md | 2 +- mempool/clist_mempool_test.go | 2 +- rpc/client/mock/abci.go | 12 +- state/execution_test.go | 4 +- 16 files changed, 127 insertions(+), 116 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 63cd05ae2..ed835aebc 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -23,6 +23,7 @@ If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID + - [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI interface * Blockchain Protocol diff --git a/abci/client/local_client.go b/abci/client/local_client.go index d0e50c330..4a3e6a5ee 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -85,7 +85,7 @@ func (app *localClient) DeliverTxAsync(tx []byte) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.DeliverTx(tx) + res := app.Application.DeliverTx(types.RequestDeliverTx{Tx: tx}) return app.callback( types.ToRequestDeliverTx(tx), types.ToResponseDeliverTx(res), @@ -96,7 +96,7 @@ func (app *localClient) CheckTxAsync(tx []byte) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.CheckTx(tx) + res := app.Application.CheckTx(types.RequestCheckTx{Tx: tx}) return app.callback( types.ToRequestCheckTx(tx), types.ToResponseCheckTx(res), @@ -188,7 +188,7 @@ func (app *localClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, erro app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.DeliverTx(tx) + res := app.Application.DeliverTx(types.RequestDeliverTx{Tx: tx}) return &res, nil } @@ -196,7 +196,7 @@ func (app *localClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.CheckTx(tx) + res := app.Application.CheckTx(types.RequestCheckTx{Tx: tx}) return &res, nil } diff --git a/abci/example/counter/counter.go b/abci/example/counter/counter.go index a77e7821f..2cea1e558 100644 --- a/abci/example/counter/counter.go +++ b/abci/example/counter/counter.go @@ -42,15 +42,15 @@ func (app *CounterApplication) SetOption(req types.RequestSetOption) types.Respo return types.ResponseSetOption{} } -func (app *CounterApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { +func (app *CounterApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { if app.serial { - if len(tx) > 8 { + if len(req.Tx) > 8 { return types.ResponseDeliverTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(tx))} + Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(req.Tx))} } tx8 := make([]byte, 8) - copy(tx8[len(tx8)-len(tx):], tx) + copy(tx8[len(tx8)-len(req.Tx):], req.Tx) txValue := binary.BigEndian.Uint64(tx8) if txValue != uint64(app.txCount) { return types.ResponseDeliverTx{ @@ -62,15 +62,15 @@ func (app *CounterApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { return types.ResponseDeliverTx{Code: code.CodeTypeOK} } -func (app *CounterApplication) CheckTx(tx []byte) types.ResponseCheckTx { +func (app *CounterApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { if app.serial { - if len(tx) > 8 { + if len(req.Tx) > 8 { return types.ResponseCheckTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(tx))} + Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(req.Tx))} } tx8 := make([]byte, 8) - copy(tx8[len(tx8)-len(tx):], tx) + copy(tx8[len(tx8)-len(req.Tx):], req.Tx) txValue := binary.BigEndian.Uint64(tx8) if txValue < uint64(app.txCount) { return types.ResponseCheckTx{ diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index 0c28813f2..82d404c76 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -76,13 +76,13 @@ func (app *KVStoreApplication) Info(req types.RequestInfo) (resInfo types.Respon } // tx is either "key=value" or just arbitrary bytes -func (app *KVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { +func (app *KVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { var key, value []byte - parts := bytes.Split(tx, []byte("=")) + parts := bytes.Split(req.Tx, []byte("=")) if len(parts) == 2 { key, value = parts[0], parts[1] } else { - key, value = tx, tx + key, value = req.Tx, req.Tx } app.state.db.Set(prefixKey(key), value) @@ -101,7 +101,7 @@ func (app *KVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { return types.ResponseDeliverTx{Code: code.CodeTypeOK, Events: events} } -func (app *KVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx { +func (app *KVStoreApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { return types.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1} } diff --git a/abci/example/kvstore/kvstore_test.go b/abci/example/kvstore/kvstore_test.go index a18fb8d3c..074baa49f 100644 --- a/abci/example/kvstore/kvstore_test.go +++ b/abci/example/kvstore/kvstore_test.go @@ -19,10 +19,11 @@ import ( ) func testKVStore(t *testing.T, app types.Application, tx []byte, key, value string) { - ar := app.DeliverTx(tx) + req := types.RequestDeliverTx{Tx: tx} + ar := app.DeliverTx(req) require.False(t, ar.IsErr(), ar) // repeating tx doesn't raise error - ar = app.DeliverTx(tx) + ar = app.DeliverTx(req) require.False(t, ar.IsErr(), ar) // make sure query is fine @@ -179,7 +180,7 @@ func makeApplyBlock(t *testing.T, kvstore types.Application, heightInt int, diff kvstore.BeginBlock(types.RequestBeginBlock{Hash: hash, Header: header}) for _, tx := range txs { - if r := kvstore.DeliverTx(tx); r.IsErr() { + if r := kvstore.DeliverTx(types.RequestDeliverTx{Tx: tx}); r.IsErr() { t.Fatal(r) } } diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index f969eebfe..b7484a4aa 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -61,21 +61,21 @@ func (app *PersistentKVStoreApplication) SetOption(req types.RequestSetOption) t } // tx is either "val:pubkey/power" or "key=value" or just arbitrary bytes -func (app *PersistentKVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { +func (app *PersistentKVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { // if it starts with "val:", update the validator set // format is "val:pubkey/power" - if isValidatorTx(tx) { + if isValidatorTx(req.Tx) { // update validators in the merkle tree // and in app.ValUpdates - return app.execValidatorTx(tx) + return app.execValidatorTx(req.Tx) } // otherwise, update the key-value store - return app.app.DeliverTx(tx) + return app.app.DeliverTx(req) } -func (app *PersistentKVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx { - return app.app.CheckTx(tx) +func (app *PersistentKVStoreApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { + return app.app.CheckTx(req) } // Commit will panic if InitChain was not called diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 4b92f04cf..96cb844b7 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -178,10 +178,10 @@ func (s *SocketServer) handleRequest(req *types.Request, responses chan<- *types res := s.app.SetOption(*r.SetOption) responses <- types.ToResponseSetOption(res) case *types.Request_DeliverTx: - res := s.app.DeliverTx(r.DeliverTx.Tx) + res := s.app.DeliverTx(*r.DeliverTx) responses <- types.ToResponseDeliverTx(res) case *types.Request_CheckTx: - res := s.app.CheckTx(r.CheckTx.Tx) + res := s.app.CheckTx(*r.CheckTx) responses <- types.ToResponseCheckTx(res) case *types.Request_Commit: res := s.app.Commit() diff --git a/abci/types/application.go b/abci/types/application.go index 88f8d001e..90518851d 100644 --- a/abci/types/application.go +++ b/abci/types/application.go @@ -15,12 +15,12 @@ type Application interface { Query(RequestQuery) ResponseQuery // Query for state // Mempool Connection - CheckTx(tx []byte) ResponseCheckTx // Validate a tx for the mempool + CheckTx(RequestCheckTx) ResponseCheckTx // Validate a tx for the mempool // Consensus Connection InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block - DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing + DeliverTx(RequestDeliverTx) ResponseDeliverTx // Deliver a tx for full processing EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set Commit() ResponseCommit // Commit the state and return the application Merkle root hash } @@ -45,11 +45,11 @@ func (BaseApplication) SetOption(req RequestSetOption) ResponseSetOption { return ResponseSetOption{} } -func (BaseApplication) DeliverTx(tx []byte) ResponseDeliverTx { +func (BaseApplication) DeliverTx(req RequestDeliverTx) ResponseDeliverTx { return ResponseDeliverTx{Code: CodeTypeOK} } -func (BaseApplication) CheckTx(tx []byte) ResponseCheckTx { +func (BaseApplication) CheckTx(req RequestCheckTx) ResponseCheckTx { return ResponseCheckTx{Code: CodeTypeOK} } @@ -103,12 +103,12 @@ func (app *GRPCApplication) SetOption(ctx context.Context, req *RequestSetOption } func (app *GRPCApplication) DeliverTx(ctx context.Context, req *RequestDeliverTx) (*ResponseDeliverTx, error) { - res := app.app.DeliverTx(req.Tx) + res := app.app.DeliverTx(*req) return &res, nil } func (app *GRPCApplication) CheckTx(ctx context.Context, req *RequestCheckTx) (*ResponseCheckTx, error) { - res := app.app.CheckTx(req.Tx) + res := app.app.CheckTx(*req) return &res, nil } diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 1a4a87d08..cb3598668 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -291,11 +291,11 @@ func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { return abci.ResponseEndBlock{} } -func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { +func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { return abci.ResponseDeliverTx{Events: []abci.Event{}} } -func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { +func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { return abci.ResponseCheckTx{} } diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index af15a1fef..d9feef9b4 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -141,7 +141,7 @@ func TestMempoolRmBadTx(t *testing.T) { txBytes := make([]byte, 8) binary.BigEndian.PutUint64(txBytes, uint64(0)) - resDeliver := app.DeliverTx(txBytes) + resDeliver := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) assert.False(t, resDeliver.IsErr(), fmt.Sprintf("expected no error. got %v", resDeliver)) resCommit := app.Commit() @@ -209,8 +209,8 @@ func (app *CounterApplication) Info(req abci.RequestInfo) abci.ResponseInfo { return abci.ResponseInfo{Data: fmt.Sprintf("txs:%v", app.txCount)} } -func (app *CounterApplication) DeliverTx(tx []byte) abci.ResponseDeliverTx { - txValue := txAsUint64(tx) +func (app *CounterApplication) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { + txValue := txAsUint64(req.Tx) if txValue != uint64(app.txCount) { return abci.ResponseDeliverTx{ Code: code.CodeTypeBadNonce, @@ -220,8 +220,8 @@ func (app *CounterApplication) DeliverTx(tx []byte) abci.ResponseDeliverTx { return abci.ResponseDeliverTx{Code: code.CodeTypeOK} } -func (app *CounterApplication) CheckTx(tx []byte) abci.ResponseCheckTx { - txValue := txAsUint64(tx) +func (app *CounterApplication) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { + txValue := txAsUint64(req.Tx) if txValue != uint64(app.mempoolTxCount) { return abci.ResponseCheckTx{ Code: code.CodeTypeBadNonce, diff --git a/consensus/replay.go b/consensus/replay.go index 794f870d0..2c4377ffa 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -515,7 +515,7 @@ type mockProxyApp struct { abciResponses *sm.ABCIResponses } -func (mock *mockProxyApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { +func (mock *mockProxyApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { r := mock.abciResponses.DeliverTx[mock.txCount] mock.txCount++ if r == nil { //it could be nil because of amino unMarshall, it will cause an empty ResponseDeliverTx to become nil diff --git a/docs/app-dev/app-development.md b/docs/app-dev/app-development.md index 1b4e26800..c9983beaa 100644 --- a/docs/app-dev/app-development.md +++ b/docs/app-dev/app-development.md @@ -101,8 +101,8 @@ mempool state (this behaviour can be turned off with In go: ``` -func (app *KVStoreApplication) CheckTx(tx []byte) types.Result { - return types.OK +func (app *KVStoreApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { + return types.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1} } ``` @@ -168,14 +168,29 @@ In go: ``` // tx is either "key=value" or just arbitrary bytes -func (app *KVStoreApplication) DeliverTx(tx []byte) types.Result { - parts := strings.Split(string(tx), "=") - if len(parts) == 2 { - app.state.Set([]byte(parts[0]), []byte(parts[1])) - } else { - app.state.Set(tx, tx) - } - return types.OK +func (app *KVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { + var key, value []byte + parts := bytes.Split(req.Tx, []byte("=")) + if len(parts) == 2 { + key, value = parts[0], parts[1] + } else { + key, value = req.Tx, req.Tx + } + + app.state.db.Set(prefixKey(key), value) + app.state.Size += 1 + + events := []types.Event{ + { + Type: "app", + Attributes: []cmn.KVPair{ + {Key: []byte("creator"), Value: []byte("Cosmoshi Netowoko")}, + {Key: []byte("key"), Value: key}, + }, + }, + } + + return types.ResponseDeliverTx{Code: code.CodeTypeOK, Events: events} } ``` @@ -223,9 +238,14 @@ job of the [Handshake](#handshake). In go: ``` -func (app *KVStoreApplication) Commit() types.Result { - hash := app.state.Hash() - return types.NewResultOK(hash, "") +func (app *KVStoreApplication) Commit() types.ResponseCommit { + // Using a memdb - just return the big endian size of the db + appHash := make([]byte, 8) + binary.PutVarint(appHash, app.state.Size) + app.state.AppHash = appHash + app.state.Height += 1 + saveState(app.state) + return types.ResponseCommit{Data: appHash} } ``` @@ -256,12 +276,10 @@ In go: ``` // Track the block hash and header information -func (app *PersistentKVStoreApplication) BeginBlock(params types.RequestBeginBlock) { - // update latest block info - app.blockHeader = params.Header - - // reset valset changes - app.changes = make([]*types.Validator, 0) +func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock { + // reset valset changes + app.ValUpdates = make([]types.ValidatorUpdate, 0) + return types.ResponseBeginBlock{} } ``` @@ -303,7 +321,7 @@ In go: ``` // Update the validator set func (app *PersistentKVStoreApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock { - return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} + return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} } ``` @@ -347,43 +365,29 @@ Note: these query formats are subject to change! In go: ``` - func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { - if reqQuery.Prove { - value, proof, exists := app.state.GetWithProof(reqQuery.Data) - resQuery.Index = -1 // TODO make Proof return index - resQuery.Key = reqQuery.Data - resQuery.Value = value - resQuery.Proof = proof - if exists { - resQuery.Log = "exists" - } else { - resQuery.Log = "does not exist" - } - return - } else { - index, value, exists := app.state.Get(reqQuery.Data) - resQuery.Index = int64(index) - resQuery.Value = value - if exists { - resQuery.Log = "exists" - } else { - resQuery.Log = "does not exist" - } - return - } - } - return - } else { - index, value, exists := app.state.Get(reqQuery.Data) - resQuery.Index = int64(index) - resQuery.Value = value - if exists { - resQuery.Log = "exists" - } else { - resQuery.Log = "does not exist" - } - return - } +func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { + if reqQuery.Prove { + value := app.state.db.Get(prefixKey(reqQuery.Data)) + resQuery.Index = -1 // TODO make Proof return index + resQuery.Key = reqQuery.Data + resQuery.Value = value + if value != nil { + resQuery.Log = "exists" + } else { + resQuery.Log = "does not exist" + } + return + } else { + resQuery.Key = reqQuery.Data + value := app.state.db.Get(prefixKey(reqQuery.Data)) + resQuery.Value = value + if value != nil { + resQuery.Log = "exists" + } else { + resQuery.Log = "does not exist" + } + return + } } ``` @@ -439,7 +443,11 @@ In go: ``` func (app *KVStoreApplication) Info(req types.RequestInfo) (resInfo types.ResponseInfo) { - return types.ResponseInfo{Data: fmt.Sprintf("{\"size\":%v}", app.state.Size())} + return types.ResponseInfo{ + Data: fmt.Sprintf("{\"size\":%v}", app.state.Size), + Version: version.ABCIVersion, + AppVersion: ProtocolVersion.Uint64(), + } } ``` @@ -463,13 +471,14 @@ In go: ``` // Save the validators in the merkle tree -func (app *PersistentKVStoreApplication) InitChain(params types.RequestInitChain) { - for _, v := range params.Validators { - r := app.updateValidator(v) - if r.IsErr() { - app.logger.Error("Error updating validators", "r", r) - } - } +func (app *PersistentKVStoreApplication) InitChain(req types.RequestInitChain) types.ResponseInitChain { + for _, v := range req.Validators { + r := app.updateValidator(v) + if r.IsErr() { + app.logger.Error("Error updating validators", "r", r) + } + } + return types.ResponseInitChain{} } ``` diff --git a/docs/app-dev/indexing-transactions.md b/docs/app-dev/indexing-transactions.md index de8336a43..ffe8b989a 100644 --- a/docs/app-dev/indexing-transactions.md +++ b/docs/app-dev/indexing-transactions.md @@ -47,7 +47,7 @@ pairs of UTF-8 encoded strings (e.g. "account.owner": "Bob", "balance": Example: ``` -func (app *KVStoreApplication) DeliverTx(tx []byte) types.Result { +func (app *KVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.Result { ... tags := []cmn.KVPair{ {[]byte("account.name"), []byte("igor")}, diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index bf2c61dd7..db6e800b8 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -99,7 +99,7 @@ func TestReapMaxBytesMaxGas(t *testing.T) { checkTxs(t, mempool, 1, UnknownPeerID) tx0 := mempool.TxsFront().Value.(*mempoolTx) // assert that kv store has gas wanted = 1. - require.Equal(t, app.CheckTx(tx0.tx).GasWanted, int64(1), "KVStore had a gas value neq to 1") + require.Equal(t, app.CheckTx(abci.RequestCheckTx{Tx: tx0.tx}).GasWanted, int64(1), "KVStore had a gas value neq to 1") require.Equal(t, tx0.gasWanted, int64(1), "transactions gas was set incorrectly") // ensure each tx is 20 bytes long require.Equal(t, len(tx0.tx), 20, "Tx is longer than 20 bytes") diff --git a/rpc/client/mock/abci.go b/rpc/client/mock/abci.go index 2ab62a420..f40755fec 100644 --- a/rpc/client/mock/abci.go +++ b/rpc/client/mock/abci.go @@ -45,29 +45,29 @@ func (a ABCIApp) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts clien // TODO: Make it wait for a commit and set res.Height appropriately. func (a ABCIApp) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { res := ctypes.ResultBroadcastTxCommit{} - res.CheckTx = a.App.CheckTx(tx) + res.CheckTx = a.App.CheckTx(abci.RequestCheckTx{Tx: tx}) if res.CheckTx.IsErr() { return &res, nil } - res.DeliverTx = a.App.DeliverTx(tx) + res.DeliverTx = a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) res.Height = -1 // TODO return &res, nil } func (a ABCIApp) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - c := a.App.CheckTx(tx) + c := a.App.CheckTx(abci.RequestCheckTx{Tx: tx}) // and this gets written in a background thread... if !c.IsErr() { - go func() { a.App.DeliverTx(tx) }() // nolint: errcheck + go func() { a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) }() // nolint: errcheck } return &ctypes.ResultBroadcastTx{Code: c.Code, Data: c.Data, Log: c.Log, Hash: tx.Hash()}, nil } func (a ABCIApp) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - c := a.App.CheckTx(tx) + c := a.App.CheckTx(abci.RequestCheckTx{Tx: tx}) // and this gets written in a background thread... if !c.IsErr() { - go func() { a.App.DeliverTx(tx) }() // nolint: errcheck + go func() { a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) }() // nolint: errcheck } return &ctypes.ResultBroadcastTx{Code: c.Code, Data: c.Data, Log: c.Log, Hash: tx.Hash()}, nil } diff --git a/state/execution_test.go b/state/execution_test.go index 80b442e3d..269f489a9 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -453,11 +453,11 @@ func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { return abci.ResponseEndBlock{ValidatorUpdates: app.ValidatorUpdates} } -func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { +func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { return abci.ResponseDeliverTx{Events: []abci.Event{}} } -func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { +func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { return abci.ResponseCheckTx{} } From 866b343c0c864c6ae4369c88506d17572513ce24 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 21 Jun 2019 07:58:32 +0200 Subject: [PATCH 068/211] Changes to files that had linting issue (#3731) - Govet issues fixed - 1 gosec issue solved using nolint Signed-off-by: Marko Baricevic --- .golangci.yml | 1 - benchmarks/codec_test.go | 2 +- blockchain/reactor_test.go | 2 +- consensus/byzantine_test.go | 4 ++-- consensus/common_test.go | 4 ++-- consensus/reactor.go | 4 ---- consensus/state_test.go | 4 ++-- consensus/types/height_vote_set_test.go | 2 +- evidence/pool_test.go | 2 +- go.sum | 1 + node/node_test.go | 8 +++---- p2p/upnp/upnp.go | 2 +- privval/file_test.go | 14 ++++++------ state/execution_test.go | 20 ++++++++--------- state/state_test.go | 30 ++++++++++++------------- state/tx_filter_test.go | 2 +- state/txindex/indexer_service_test.go | 4 ++-- types/block.go | 2 +- types/protobuf_test.go | 2 +- 19 files changed, 53 insertions(+), 57 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index a051e1a45..6adbbd9da 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,7 +20,6 @@ linters: - gochecknoinits - scopelint - stylecheck - # linters-settings: # govet: # check-shadowing: true diff --git a/benchmarks/codec_test.go b/benchmarks/codec_test.go index eff5c7349..64c0e72cf 100644 --- a/benchmarks/codec_test.go +++ b/benchmarks/codec_test.go @@ -14,7 +14,7 @@ import ( func testNodeInfo(id p2p.ID) p2p.DefaultNodeInfo { return p2p.DefaultNodeInfo{ - ProtocolVersion: p2p.ProtocolVersion{1, 2, 3}, + ProtocolVersion: p2p.ProtocolVersion{P2P: 1, Block: 2, App: 3}, ID_: id, Moniker: "SOMENAME", Network: "SOMENAME", diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index cb3598668..b5137bb2a 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -111,7 +111,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals thisBlock := makeBlock(blockHeight, state, lastCommit) thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) - blockID := types.BlockID{thisBlock.Hash(), thisParts.Header()} + blockID := types.BlockID{Hash: thisBlock.Hash(), PartsHeader: thisParts.Header()} state, err = blockExec.ApplyBlock(state, blockID, thisBlock) if err != nil { diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index c2eb114dc..1c52e79ad 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -177,7 +177,7 @@ func byzantineDecideProposalFunc(t *testing.T, height int64, round int, cs *Cons // Create a new proposal block from state/txs from the mempool. block1, blockParts1 := cs.createProposalBlock() - polRound, propBlockID := cs.ValidRound, types.BlockID{block1.Hash(), blockParts1.Header()} + polRound, propBlockID := cs.ValidRound, types.BlockID{Hash: block1.Hash(), PartsHeader: blockParts1.Header()} proposal1 := types.NewProposal(height, round, polRound, propBlockID) if err := cs.privValidator.SignProposal(cs.state.ChainID, proposal1); err != nil { t.Error(err) @@ -185,7 +185,7 @@ func byzantineDecideProposalFunc(t *testing.T, height int64, round int, cs *Cons // Create a new proposal block from state/txs from the mempool. block2, blockParts2 := cs.createProposalBlock() - polRound, propBlockID = cs.ValidRound, types.BlockID{block2.Hash(), blockParts2.Header()} + polRound, propBlockID = cs.ValidRound, types.BlockID{Hash: block2.Hash(), PartsHeader: blockParts2.Header()} proposal2 := types.NewProposal(height, round, polRound, propBlockID) if err := cs.privValidator.SignProposal(cs.state.ChainID, proposal2); err != nil { t.Error(err) diff --git a/consensus/common_test.go b/consensus/common_test.go index a4ad79c94..29db524ec 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -86,7 +86,7 @@ func (vs *validatorStub) signVote(voteType types.SignedMsgType, hash []byte, hea Round: vs.Round, Timestamp: tmtime.Now(), Type: voteType, - BlockID: types.BlockID{hash, header}, + BlockID: types.BlockID{Hash: hash, PartsHeader: header}, } err := vs.PrivValidator.SignVote(config.ChainID(), vote) return vote, err @@ -159,7 +159,7 @@ func decideProposal(cs1 *ConsensusState, vs *validatorStub, height int64, round } // Make proposal - polRound, propBlockID := validRound, types.BlockID{block.Hash(), blockParts.Header()} + polRound, propBlockID := validRound, types.BlockID{Hash: block.Hash(), PartsHeader: blockParts.Header()} proposal = types.NewProposal(height, round, polRound, propBlockID) if err := vs.SignProposal(chainID, proposal); err != nil { panic(err) diff --git a/consensus/reactor.go b/consensus/reactor.go index 36e948f6d..f690a407d 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -351,10 +351,6 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) default: conR.Logger.Error(fmt.Sprintf("Unknown chId %X", chID)) } - - if err != nil { - conR.Logger.Error("Error in Receive()", "err", err) - } } // SetEventBus sets event bus. diff --git a/consensus/state_test.go b/consensus/state_test.go index 87e351dc8..93ef0d4cb 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -192,7 +192,7 @@ func TestStateBadProposal(t *testing.T) { stateHash[0] = byte((stateHash[0] + 1) % 255) propBlock.AppHash = stateHash propBlockParts := propBlock.MakePartSet(partSize) - blockID := types.BlockID{propBlock.Hash(), propBlockParts.Header()} + blockID := types.BlockID{Hash: propBlock.Hash(), PartsHeader: propBlockParts.Header()} proposal := types.NewProposal(vs2.Height, round, -1, blockID) if err := vs2.SignProposal(config.ChainID(), proposal); err != nil { t.Fatal("failed to sign bad proposal", err) @@ -811,7 +811,7 @@ func TestStateLockPOLSafety2(t *testing.T) { _, propBlock0 := decideProposal(cs1, vss[0], height, round) propBlockHash0 := propBlock0.Hash() propBlockParts0 := propBlock0.MakePartSet(partSize) - propBlockID0 := types.BlockID{propBlockHash0, propBlockParts0.Header()} + propBlockID0 := types.BlockID{Hash: propBlockHash0, PartsHeader: propBlockParts0.Header()} // the others sign a polka but we don't see it prevotes := signVotes(types.PrevoteType, propBlockHash0, propBlockParts0.Header(), vs2, vs3, vs4) diff --git a/consensus/types/height_vote_set_test.go b/consensus/types/height_vote_set_test.go index 42b5333a1..f45492aa4 100644 --- a/consensus/types/height_vote_set_test.go +++ b/consensus/types/height_vote_set_test.go @@ -62,7 +62,7 @@ func makeVoteHR(t *testing.T, height int64, round int, privVals []types.PrivVali Round: round, Timestamp: tmtime.Now(), Type: types.PrecommitType, - BlockID: types.BlockID{[]byte("fakehash"), types.PartSetHeader{}}, + BlockID: types.BlockID{Hash: []byte("fakehash"), PartsHeader: types.PartSetHeader{}}, } chainID := config.ChainID() err := privVal.SignVote(chainID, vote) diff --git a/evidence/pool_test.go b/evidence/pool_test.go index 30b20011e..13bc45563 100644 --- a/evidence/pool_test.go +++ b/evidence/pool_test.go @@ -60,7 +60,7 @@ func TestEvidencePool(t *testing.T) { pool := NewEvidencePool(stateDB, evidenceDB) goodEvidence := types.NewMockGoodEvidence(height, 0, valAddr) - badEvidence := types.MockBadEvidence{goodEvidence} + badEvidence := types.MockBadEvidence{MockGoodEvidence: goodEvidence} // bad evidence err := pool.AddEvidence(badEvidence) diff --git a/go.sum b/go.sum index 10e54c0fd..fee691de9 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/etcd-io/bbolt v1.3.2 h1:RLRQ0TKLX7DlBRXAJHvbmXL17Q3KNnTBtZ9B6Qo+/Y0= github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/node/node_test.go b/node/node_test.go index 6971ddd34..ce4e82c2d 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -285,10 +285,10 @@ func state(nVals int, height int64) (sm.State, dbm.DB) { secret := []byte(fmt.Sprintf("test%d", i)) pk := ed25519.GenPrivKeyFromSecret(secret) vals[i] = types.GenesisValidator{ - pk.PubKey().Address(), - pk.PubKey(), - 1000, - fmt.Sprintf("test%d", i), + Address: pk.PubKey().Address(), + PubKey: pk.PubKey(), + Power: 1000, + Name: fmt.Sprintf("test%d", i), } } s, _ := sm.MakeGenesisState(&types.GenesisDoc{ diff --git a/p2p/upnp/upnp.go b/p2p/upnp/upnp.go index d53974fc4..89f35c5df 100644 --- a/p2p/upnp/upnp.go +++ b/p2p/upnp/upnp.go @@ -197,7 +197,7 @@ func localIPv4() (net.IP, error) { } func getServiceURL(rootURL string) (url, urnDomain string, err error) { - r, err := http.Get(rootURL) + r, err := http.Get(rootURL) // nolint: gosec if err != nil { return } diff --git a/privval/file_test.go b/privval/file_test.go index 06d75a809..98de69480 100644 --- a/privval/file_test.go +++ b/privval/file_test.go @@ -50,7 +50,7 @@ func TestResetValidator(t *testing.T) { // test vote height, round := int64(10), 1 voteType := byte(types.PrevoteType) - blockID := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} + blockID := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} vote := newVote(privVal.Key.Address, 0, height, round, voteType, blockID) err = privVal.SignVote("mychainid", vote) assert.NoError(t, err, "expected no error signing vote") @@ -162,8 +162,8 @@ func TestSignVote(t *testing.T) { privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) - block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} - block2 := types.BlockID{[]byte{3, 2, 1}, types.PartSetHeader{}} + block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} + block2 := types.BlockID{Hash: []byte{3, 2, 1}, PartsHeader: types.PartSetHeader{}} height, round := int64(10), 1 voteType := byte(types.PrevoteType) @@ -207,8 +207,8 @@ func TestSignProposal(t *testing.T) { privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) - block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{5, []byte{1, 2, 3}}} - block2 := types.BlockID{[]byte{3, 2, 1}, types.PartSetHeader{10, []byte{3, 2, 1}}} + block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{Total: 5, Hash: []byte{1, 2, 3}}} + block2 := types.BlockID{Hash: []byte{3, 2, 1}, PartsHeader: types.PartSetHeader{Total: 10, Hash: []byte{3, 2, 1}}} height, round := int64(10), 1 // sign a proposal for first time @@ -249,7 +249,7 @@ func TestDifferByTimestamp(t *testing.T) { privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) - block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{5, []byte{1, 2, 3}}} + block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{Total: 5, Hash: []byte{1, 2, 3}}} height, round := int64(10), 1 chainID := "mychainid" @@ -277,7 +277,7 @@ func TestDifferByTimestamp(t *testing.T) { // test vote { voteType := byte(types.PrevoteType) - blockID := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} + blockID := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} vote := newVote(privVal.Key.Address, 0, height, round, voteType, blockID) err := privVal.SignVote("mychainid", vote) assert.NoError(t, err, "expected no error signing vote") diff --git a/state/execution_test.go b/state/execution_test.go index 269f489a9..ac7b9cc38 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -40,7 +40,7 @@ func TestApplyBlock(t *testing.T) { mock.Mempool{}, MockEvidencePool{}) block := makeBlock(state, 1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} //nolint:ineffassign state, err = blockExec.ApplyBlock(state, blockID, block) @@ -62,7 +62,7 @@ func TestBeginBlockValidators(t *testing.T) { prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} - prevBlockID := types.BlockID{prevHash, prevParts} + prevBlockID := types.BlockID{Hash: prevHash, PartsHeader: prevParts} now := tmtime.Now() commitSig0 := (&types.Vote{ValidatorIndex: 0, Timestamp: now, Type: types.PrecommitType}).CommitSig() @@ -115,7 +115,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} - prevBlockID := types.BlockID{prevHash, prevParts} + prevBlockID := types.BlockID{Hash: prevHash, PartsHeader: prevParts} height1, idx1, val1 := int64(8), 0, state.Validators.Validators[0].Address height2, idx2, val2 := int64(3), 1, state.Validators.Validators[1].Address @@ -159,7 +159,7 @@ func TestValidateValidatorUpdates(t *testing.T) { secpKey := secp256k1.GenPrivKey().PubKey() - defaultValidatorParams := types.ValidatorParams{[]string{types.ABCIPubKeyTypeEd25519}} + defaultValidatorParams := types.ValidatorParams{PubKeyTypes: []string{types.ABCIPubKeyTypeEd25519}} testCases := []struct { name string @@ -321,7 +321,7 @@ func TestEndBlockValidatorUpdates(t *testing.T) { require.NoError(t, err) block := makeBlock(state, 1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} pubkey := ed25519.GenPrivKey().PubKey() app.ValidatorUpdates = []abci.ValidatorUpdate{ @@ -369,7 +369,7 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) block := makeBlock(state, 1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} // Remove the only validator app.ValidatorUpdates = []abci.ValidatorUpdate{ @@ -398,10 +398,10 @@ func state(nVals, height int) (State, dbm.DB) { secret := []byte(fmt.Sprintf("test%d", i)) pk := ed25519.GenPrivKeyFromSecret(secret) vals[i] = types.GenesisValidator{ - pk.PubKey().Address(), - pk.PubKey(), - 1000, - fmt.Sprintf("test%d", i), + Address: pk.PubKey().Address(), + PubKey: pk.PubKey(), + Power: 1000, + Name: fmt.Sprintf("test%d", i), } } s, _ := MakeGenesisState(&types.GenesisDoc{ diff --git a/state/state_test.go b/state/state_test.go index c7600cc38..1ff09ccdb 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -129,7 +129,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { {Code: 32, Data: []byte("Hello"), Log: "Huh?"}, }, types.ABCIResults{ - {32, []byte("Hello")}, + {Code: 32, Data: []byte("Hello")}, }}, 2: { []*abci.ResponseDeliverTx{ @@ -143,8 +143,8 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { }, }, types.ABCIResults{ - {383, nil}, - {0, []byte("Gotcha!")}, + {Code: 383, Data: nil}, + {Code: 0, Data: []byte("Gotcha!")}, }}, 3: { nil, @@ -404,7 +404,7 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { assert.EqualValues(t, 0, val1.ProposerPriority) block := makeBlock(state, state.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} abciResponses := &ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } @@ -514,7 +514,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { assert.Equal(t, val1PubKey.Address(), state.Validators.Proposer.Address) block := makeBlock(state, state.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} // no updates: abciResponses := &ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, @@ -667,7 +667,7 @@ func TestLargeGenesisValidator(t *testing.T) { require.NoError(t, err) block := makeBlock(oldState, oldState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -693,7 +693,7 @@ func TestLargeGenesisValidator(t *testing.T) { EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -707,7 +707,7 @@ func TestLargeGenesisValidator(t *testing.T) { require.NoError(t, err) block := makeBlock(lastState, lastState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedStateInner, err := updateState(lastState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -738,7 +738,7 @@ func TestLargeGenesisValidator(t *testing.T) { EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{addedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} state, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) } @@ -750,7 +750,7 @@ func TestLargeGenesisValidator(t *testing.T) { EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal}}, } block = makeBlock(oldState, oldState.LastBlockHeight+1) - blockID = types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) updatedState, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) @@ -770,7 +770,7 @@ func TestLargeGenesisValidator(t *testing.T) { validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) block = makeBlock(curState, curState.LastBlockHeight+1) - blockID = types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} curState, err = updateState(curState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) if !bytes.Equal(curState.Validators.Proposer.Address, curState.NextValidators.Proposer.Address) { @@ -794,7 +794,7 @@ func TestLargeGenesisValidator(t *testing.T) { require.NoError(t, err) block := makeBlock(updatedState, updatedState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedState, err = updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -1033,7 +1033,7 @@ func makeHeaderPartsResponsesValPubKeyChange(state State, height int64, } } - return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses } func makeHeaderPartsResponsesValPowerChange(state State, height int64, @@ -1054,7 +1054,7 @@ func makeHeaderPartsResponsesValPowerChange(state State, height int64, } } - return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses } func makeHeaderPartsResponsesParams(state State, height int64, @@ -1064,7 +1064,7 @@ func makeHeaderPartsResponsesParams(state State, height int64, abciResponses := &ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(¶ms)}, } - return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses } type paramsChangeTestCase struct { diff --git a/state/tx_filter_test.go b/state/tx_filter_test.go index ffb41c178..e48ad2c3e 100644 --- a/state/tx_filter_test.go +++ b/state/tx_filter_test.go @@ -51,7 +51,7 @@ func randomGenesisDoc() *types.GenesisDoc { return &types.GenesisDoc{ GenesisTime: tmtime.Now(), ChainID: "abc", - Validators: []types.GenesisValidator{{pubkey.Address(), pubkey, 10, "myval"}}, + Validators: []types.GenesisValidator{{Address: pubkey.Address(), PubKey: pubkey, Power: 10, Name: "myval"}}, ConsensusParams: types.DefaultConsensusParams(), } } diff --git a/state/txindex/indexer_service_test.go b/state/txindex/indexer_service_test.go index 982d7b8c4..079f9cec2 100644 --- a/state/txindex/indexer_service_test.go +++ b/state/txindex/indexer_service_test.go @@ -43,14 +43,14 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) { Tx: types.Tx("foo"), Result: abci.ResponseDeliverTx{Code: 0}, } - eventBus.PublishEventTx(types.EventDataTx{*txResult1}) + eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult1}) txResult2 := &types.TxResult{ Height: 1, Index: uint32(1), Tx: types.Tx("bar"), Result: abci.ResponseDeliverTx{Code: 0}, } - eventBus.PublishEventTx(types.EventDataTx{*txResult2}) + eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult2}) time.Sleep(100 * time.Millisecond) diff --git a/types/block.go b/types/block.go index 313eb6b75..55709ad60 100644 --- a/types/block.go +++ b/types/block.go @@ -198,7 +198,7 @@ func (b *Block) Hash() cmn.HexBytes { b.mtx.Lock() defer b.mtx.Unlock() - if b == nil || b.LastCommit == nil { + if b.LastCommit == nil { return nil } b.fillHeader() diff --git a/types/protobuf_test.go b/types/protobuf_test.go index 64caa3f4c..833c7dc38 100644 --- a/types/protobuf_test.go +++ b/types/protobuf_test.go @@ -90,7 +90,7 @@ func TestABCIHeader(t *testing.T) { height, numTxs, []byte("lastCommitHash"), []byte("dataHash"), []byte("evidenceHash"), ) - protocolVersion := version.Consensus{7, 8} + protocolVersion := version.Consensus{Block: 7, App: 8} timestamp := time.Now() lastBlockID := BlockID{ Hash: []byte("hash"), From 2e5b2a9537fd68d32c39693b810d2da61c94b73b Mon Sep 17 00:00:00 2001 From: needkane Date: Fri, 21 Jun 2019 19:18:49 +0800 Subject: [PATCH 069/211] abci/examples: switch from hex to base64 pubkey in kvstore (#3641) * abci/example: use base64 for update validator set * update kvstore/README.md * update CHANGELOG_PENDING.md and abci/example/kvstore/README.md --- CHANGELOG_PENDING.md | 1 + abci/example/kvstore/README.md | 6 +++--- abci/example/kvstore/persistent_kvstore.go | 24 ++++++++++++---------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index ed835aebc..74aa72c6c 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -34,6 +34,7 @@ ### IMPROVEMENTS: - [p2p] \#3666 Add per channel telemetry to improve reactor observability - [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) +- [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) ### BUG FIXES: - [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) diff --git a/abci/example/kvstore/README.md b/abci/example/kvstore/README.md index e988eadb0..bed81a598 100644 --- a/abci/example/kvstore/README.md +++ b/abci/example/kvstore/README.md @@ -22,10 +22,10 @@ and the Handshake allows any necessary blocks to be replayed. Validator set changes are effected using the following transaction format: ``` -val:pubkey1/power1,addr2/power2,addr3/power3" +"val:pubkey1!power1,pubkey2!power2,pubkey3!power3" ``` -where `power1` is the new voting power for the validator with `pubkey1` (possibly a new one). +where `pubkeyN` is a base64-encoded 32-byte ed25519 key and `powerN` is a new voting power for the validator with `pubkeyN` (possibly a new one). +To remove a validator from the validator set, set power to `0`. There is no sybil protection against new validators joining. -Validators can be removed by setting their power to `0`. diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index b7484a4aa..ba0b53896 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -2,7 +2,7 @@ package kvstore import ( "bytes" - "encoding/hex" + "encoding/base64" "fmt" "strconv" "strings" @@ -60,10 +60,10 @@ func (app *PersistentKVStoreApplication) SetOption(req types.RequestSetOption) t return app.app.SetOption(req) } -// tx is either "val:pubkey/power" or "key=value" or just arbitrary bytes +// tx is either "val:pubkey!power" or "key=value" or just arbitrary bytes func (app *PersistentKVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { // if it starts with "val:", update the validator set - // format is "val:pubkey/power" + // format is "val:pubkey!power" if isValidatorTx(req.Tx) { // update validators in the merkle tree // and in app.ValUpdates @@ -129,33 +129,34 @@ func (app *PersistentKVStoreApplication) Validators() (validators []types.Valida } func MakeValSetChangeTx(pubkey types.PubKey, power int64) []byte { - return []byte(fmt.Sprintf("val:%X/%d", pubkey.Data, power)) + pubStr := base64.StdEncoding.EncodeToString(pubkey.Data) + return []byte(fmt.Sprintf("val:%s!%d", pubStr, power)) } func isValidatorTx(tx []byte) bool { return strings.HasPrefix(string(tx), ValidatorSetChangePrefix) } -// format is "val:pubkey/power" -// pubkey is raw 32-byte ed25519 key +// format is "val:pubkey!power" +// pubkey is a base64-encoded 32-byte ed25519 key func (app *PersistentKVStoreApplication) execValidatorTx(tx []byte) types.ResponseDeliverTx { tx = tx[len(ValidatorSetChangePrefix):] //get the pubkey and power - pubKeyAndPower := strings.Split(string(tx), "/") + pubKeyAndPower := strings.Split(string(tx), "!") if len(pubKeyAndPower) != 2 { return types.ResponseDeliverTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Expected 'pubkey/power'. Got %v", pubKeyAndPower)} + Log: fmt.Sprintf("Expected 'pubkey!power'. Got %v", pubKeyAndPower)} } pubkeyS, powerS := pubKeyAndPower[0], pubKeyAndPower[1] // decode the pubkey - pubkey, err := hex.DecodeString(pubkeyS) + pubkey, err := base64.StdEncoding.DecodeString(pubkeyS) if err != nil { return types.ResponseDeliverTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Pubkey (%s) is invalid hex", pubkeyS)} + Log: fmt.Sprintf("Pubkey (%s) is invalid base64", pubkeyS)} } // decode the power @@ -176,9 +177,10 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate if v.Power == 0 { // remove validator if !app.app.state.db.Has(key) { + pubStr := base64.StdEncoding.EncodeToString(v.PubKey.Data) return types.ResponseDeliverTx{ Code: code.CodeTypeUnauthorized, - Log: fmt.Sprintf("Cannot remove non-existent validator %X", key)} + Log: fmt.Sprintf("Cannot remove non-existent validator %s", pubStr)} } app.app.state.db.Delete(key) } else { From 59497c362bdd21a3cb5b2dc1afe402496eac7776 Mon Sep 17 00:00:00 2001 From: Ismail Khoffi Date: Fri, 21 Jun 2019 22:29:16 +0200 Subject: [PATCH 070/211] Prepare nuking develop (#3726) * Update Readme and contrib. guidelines: remove traces of master Signed-off-by: Ismail Khoffi * add simple example Signed-off-by: Ismail Khoffi * update contributing.md * add link * Update CONTRIBUTING.md * add a note on master and releases Signed-off-by: Ismail Khoffi * update readme --- CONTRIBUTING.md | 65 ++++++++++++++++++++++++++++++++----------------- README.md | 15 ++++++------ 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e68e6d1ee..77a625504 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,8 @@ Thank you for considering making contributions to Tendermint and related repositories! Start by taking a look at the [coding repo](https://github.com/tendermint/coding) for overall information on repository workflow and standards. -Please follow standard github best practices: fork the repo, branch from the tip of develop, make some commits, and submit a pull request to develop. See the [open issues](https://github.com/tendermint/tendermint/issues) for things we need help with! +Please follow standard github best practices: fork the repo, branch from the tip of `master`, make some commits, and submit a pull request to `master`. +See the [open issues](https://github.com/tendermint/tendermint/issues) for things we need help with! Before making a pull request, please open an issue describing the change you would like to make. If an issue for your change already exists, @@ -112,32 +113,36 @@ removed from the header in rpc responses as well. ## Branching Model and Release -We follow a variant of [git flow](http://nvie.com/posts/a-successful-git-branching-model/). -This means that all pull-requests should be made against develop. Any merge to -master constitutes a tagged release. +The main development branch is master. -Note all pull requests should be squash merged except for merging to master and -merging master back to develop. This keeps the commit history clean and makes it +Every release is maintained in a release branch named `vX.Y.Z`. + +Note all pull requests should be squash merged except for merging to a release branch (named `vX.Y`). This keeps the commit history clean and makes it easy to reference the pull request where a change was introduced. -### Development Procedure: -- the latest state of development is on `develop` -- `develop` must never fail `make test` -- never --force onto `develop` (except when reverting a broken commit, which should seldom happen) +### Development Procedure + +- the latest state of development is on `master` +- `master` must never fail `make test` +- never --force onto `master` (except when reverting a broken commit, which should seldom happen) - create a development branch either on github.com/tendermint/tendermint, or your fork (using `git remote add origin`) - make changes and update the `CHANGELOG_PENDING.md` to record your change -- before submitting a pull request, run `git rebase` on top of the latest `develop` +- before submitting a pull request, run `git rebase` on top of the latest `master` -### Pull Merge Procedure: -- ensure pull branch is based on a recent develop +### Pull Merge Procedure + +- ensure pull branch is based on a recent `master` - run `make test` to ensure that all tests pass - squash merge pull request - the `unstable` branch may be used to aggregate pull merges before fixing tests -### Release Procedure: -- start on `develop` -- run integration tests (see `test_integrations` in Makefile) -- prepare release in a pull request against develop (to be squash merged): +### Release Procedure + +#### Major Release + +1. start on `master` +2. run integration tests (see `test_integrations` in Makefile) +3. prepare release in a pull request against `master` (to be squash merged): - copy `CHANGELOG_PENDING.md` to top of `CHANGELOG.md` - run `python ./scripts/linkify_changelog.py CHANGELOG.md` to add links for all issues @@ -147,14 +152,28 @@ easy to reference the pull request where a change was introduced. ./scripts/authors.sh ` - reset the `CHANGELOG_PENDING.md` - bump versions -- push latest develop with prepared release details to release/vX.X.X to run the extended integration tests on the CI -- if necessary, make pull requests against release/vX.X.X and squash merge them -- merge to master (don't squash merge!) -- merge master back to develop (don't squash merge!) +4. push your changes with prepared release details to `vX.X` (this will trigger the release `vX.X.0`) +5. merge back to master (don't squash merge!) + +#### Minor Release + +If there were no breaking changes and you need to create a release nonetheless, +the procedure is almost exactly like with a new release above. + +The only difference is that in the end you create a pull request against the existing `X.X` branch. +The branch name should match the release number you want to create. +Merging this PR will trigger the next release. +For example, if the PR is against an existing 0.34 branch which already contains a v0.34.0 release/tag, +the patch version will be incremented and the created release will be v0.34.1. -### Hotfix Procedure: +#### Backport Release -- follow the normal development and release procedure without any differences +1. start from the existing release branch you want to backport changes to (e.g. v0.30) +Branch to a release/vX.X.X branch locally (e.g. release/v0.30.7) +2. cherry pick the commit(s) that contain the changes you want to backport (usually these commits are from squash-merged PRs which were already reviewed) +3. steps 2 and 3 from [Major Release](#major-release) +4. push changes to release/vX.X.X branch +5. open a PR against the existing vX.X branch ## Testing diff --git a/README.md b/README.md index ad4fc1308..ec7b28cc0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/6874 Branch | Tests | Coverage ----------|-------|---------- master | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/master.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/master) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/master/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) -develop | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/develop.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/develop) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/develop/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine - written in any programming language - and securely replicates it on many machines. @@ -27,13 +26,15 @@ For protocol details, see [the specification](/docs/spec). For detailed analysis of the consensus protocol, including safety and liveness proofs, see our recent paper, "[The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938)". -## A Note on Production Readiness +## Releases -While Tendermint is being used in production in private, permissioned -environments, we are still working actively to harden and audit it in preparation -for use in public blockchains, such as the [Cosmos Network](https://cosmos.network/). -We are also still making breaking changes to the protocol and the APIs. -Thus, we tag the releases as *alpha software*. +NOTE: The master branch is now an active development branch (starting with `v0.32`). Please, do not depend on it and +use [releases](https://github.com/tendermint/tendermint/releases) instead. + +Tendermint is being used in production in both private and public environments, +most notably the blockchains of the [Cosmos Network](https://cosmos.network/). +However, we are still making breaking changes to the protocol and the APIs and have not yet released v1.0. +See below for more details about [versioning](#versioning). In any case, if you intend to run Tendermint in production, please [contact us](mailto:partners@tendermint.com) and [join the chat](https://riot.im/app/#/room/#tendermint:matrix.org). From 228bba799ddc14604788899c3493cf3534e2fdfd Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 21 Jun 2019 17:29:29 -0400 Subject: [PATCH 071/211] state: add more tests for block validation (#3674) * Expose priv validators for use in testing * Generalize block header validation test past height 1 * Remove ineffectual assignment * Remove redundant SaveState call * Reorder comment for clarity * Use the block executor ApplyBlock function instead of implementing a stripped-down version of it * Remove commented-out code * Remove unnecessary test The required tests already appear to be implemented (implicitly) through the TestValidateBlockHeader test. * Allow for catching of specific error types during TestValidateBlockCommit * Make return error testable * Clean up and add TestValidateBlockCommit code * Fix formatting * Extract function to create a new mock test app * Update comment for clarity * Fix comment * Add skeleton code for evidence-related test * Allow for addressing priv val by address * Generalize test beyond a single validator * Generalize TestValidateBlockEvidence past first height * Reorder code to clearly separate tests and utility code * Use a common constant for stop height for testing in state/validation_test.go * Refactor errors to resemble existing conventions * Fix formatting * Extract common helper functions Having the tests littered with helper functions makes them less easily readable imho, so I've pulled them out into a separate file. This also makes it easier to see what helper functions are available during testing, so we minimize the chance of duplication when writing new tests. * Remove unused parameter * Remove unused parameters * Add field keys * Remove unused height constant * Fix typo * Fix incorrect return error * Add field keys * Use separate package for tests This refactors all of the state package's tests into a state_test package, so as to keep any usage of the state package's internal methods explicit. Any internal methods/constants used by tests are now explicitly exported in state/export_test.go * Refactor: extract helper function to make, validate, execute and commit a block * Rename state function to makeState * Remove redundant constant for number of validators * Refactor mock evidence registration into TestMain * Remove extraneous nVals variable * Replace function-level TODOs with file-level TODO and explanation * Remove extraneous comment * Fix linting issues brought up by GolangCI (pulled in from latest merge from develop) --- state/execution_test.go | 117 ++-------------- state/export_test.go | 62 +++++++++ state/helpers_test.go | 280 +++++++++++++++++++++++++++++++++++++++ state/main_test.go | 13 ++ state/services.go | 2 +- state/state_test.go | 219 ++++++++++-------------------- state/store_test.go | 31 ++--- state/tx_filter_test.go | 19 +-- state/validation.go | 5 +- state/validation_test.go | 221 ++++++++++++++++++------------ types/errors.go | 41 ++++++ types/validator_set.go | 4 +- 12 files changed, 640 insertions(+), 374 deletions(-) create mode 100644 state/export_test.go create mode 100644 state/helpers_test.go create mode 100644 state/main_test.go create mode 100644 types/errors.go diff --git a/state/execution_test.go b/state/execution_test.go index ac7b9cc38..38301df73 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -1,22 +1,20 @@ -package state +package state_test import ( "context" - "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/abci/example/kvstore" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/secp256k1" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" ) @@ -34,10 +32,10 @@ func TestApplyBlock(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(1, 1) + state, stateDB, _ := makeState(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), - mock.Mempool{}, MockEvidencePool{}) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), + mock.Mempool{}, sm.MockEvidencePool{}) block := makeBlock(state, 1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} @@ -58,7 +56,7 @@ func TestBeginBlockValidators(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(2, 2) + state, stateDB, _ := makeState(2, 2) prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} @@ -84,7 +82,7 @@ func TestBeginBlockValidators(t *testing.T) { // block for height 2 block, _ := state.MakeBlock(2, makeTxs(2), lastCommit, nil, state.Validators.GetProposer().Address) - _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) + _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) require.Nil(t, err, tc.desc) // -> app receives a list of validators with a bool indicating if they signed @@ -111,7 +109,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(2, 12) + state, stateDB, _ := makeState(2, 12) prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} @@ -145,7 +143,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { block, _ := state.MakeBlock(10, makeTxs(2), lastCommit, nil, state.Validators.GetProposer().Address) block.Time = now block.Evidence.Evidence = tc.evidence - _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) + _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) require.Nil(t, err, tc.desc) // -> app must receive an index of the byzantine validator @@ -213,7 +211,7 @@ func TestValidateValidatorUpdates(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := validateValidatorUpdates(tc.abciUpdates, tc.validatorParams) + err := sm.ValidateValidatorUpdates(tc.abciUpdates, tc.validatorParams) if tc.shouldErr { assert.Error(t, err) } else { @@ -307,9 +305,9 @@ func TestEndBlockValidatorUpdates(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(1, 1) + state, stateDB, _ := makeState(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) eventBus := types.NewEventBus() err = eventBus.Start() @@ -365,8 +363,8 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) + state, stateDB, _ := makeState(1, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) block := makeBlock(state, 1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} @@ -381,90 +379,3 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { assert.NotEmpty(t, state.NextValidators.Validators) } - -//---------------------------------------------------------------------------- - -// make some bogus txs -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < nTxsPerBlock; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func state(nVals, height int) (State, dbm.DB) { - vals := make([]types.GenesisValidator, nVals) - for i := 0; i < nVals; i++ { - secret := []byte(fmt.Sprintf("test%d", i)) - pk := ed25519.GenPrivKeyFromSecret(secret) - vals[i] = types.GenesisValidator{ - Address: pk.PubKey().Address(), - PubKey: pk.PubKey(), - Power: 1000, - Name: fmt.Sprintf("test%d", i), - } - } - s, _ := MakeGenesisState(&types.GenesisDoc{ - ChainID: chainID, - Validators: vals, - AppHash: nil, - }) - - // save validators to db for 2 heights - stateDB := dbm.NewMemDB() - SaveState(stateDB, s) - - for i := 1; i < height; i++ { - s.LastBlockHeight++ - s.LastValidators = s.Validators.Copy() - SaveState(stateDB, s) - } - return s, stateDB -} - -func makeBlock(state State, height int64) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(state.LastBlockHeight), new(types.Commit), nil, state.Validators.GetProposer().Address) - return block -} - -//---------------------------------------------------------------------------- - -type testApp struct { - abci.BaseApplication - - CommitVotes []abci.VoteInfo - ByzantineValidators []abci.Evidence - ValidatorUpdates []abci.ValidatorUpdate -} - -var _ abci.Application = (*testApp)(nil) - -func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) { - return abci.ResponseInfo{} -} - -func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { - app.CommitVotes = req.LastCommitInfo.Votes - app.ByzantineValidators = req.ByzantineValidators - return abci.ResponseBeginBlock{} -} - -func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { - return abci.ResponseEndBlock{ValidatorUpdates: app.ValidatorUpdates} -} - -func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { - return abci.ResponseDeliverTx{Events: []abci.Event{}} -} - -func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { - return abci.ResponseCheckTx{} -} - -func (app *testApp) Commit() abci.ResponseCommit { - return abci.ResponseCommit{} -} - -func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { - return -} diff --git a/state/export_test.go b/state/export_test.go new file mode 100644 index 000000000..af7f5cc23 --- /dev/null +++ b/state/export_test.go @@ -0,0 +1,62 @@ +package state + +import ( + abci "github.com/tendermint/tendermint/abci/types" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/types" +) + +// +// TODO: Remove dependence on all entities exported from this file. +// +// Every entity exported here is dependent on a private entity from the `state` +// package. Currently, these functions are only made available to tests in the +// `state_test` package, but we should not be relying on them for our testing. +// Instead, we should be exclusively relying on exported entities for our +// testing, and should be refactoring exported entities to make them more +// easily testable from outside of the package. +// + +const ValSetCheckpointInterval = valSetCheckpointInterval + +// UpdateState is an alias for updateState exported from execution.go, +// exclusively and explicitly for testing. +func UpdateState( + state State, + blockID types.BlockID, + header *types.Header, + abciResponses *ABCIResponses, + validatorUpdates []*types.Validator, +) (State, error) { + return updateState(state, blockID, header, abciResponses, validatorUpdates) +} + +// ValidateValidatorUpdates is an alias for validateValidatorUpdates exported +// from execution.go, exclusively and explicitly for testing. +func ValidateValidatorUpdates(abciUpdates []abci.ValidatorUpdate, params types.ValidatorParams) error { + return validateValidatorUpdates(abciUpdates, params) +} + +// CalcValidatorsKey is an alias for the private calcValidatorsKey method in +// store.go, exported exclusively and explicitly for testing. +func CalcValidatorsKey(height int64) []byte { + return calcValidatorsKey(height) +} + +// SaveABCIResponses is an alias for the private saveABCIResponses method in +// store.go, exported exclusively and explicitly for testing. +func SaveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { + saveABCIResponses(db, height, abciResponses) +} + +// SaveConsensusParamsInfo is an alias for the private saveConsensusParamsInfo +// method in store.go, exported exclusively and explicitly for testing. +func SaveConsensusParamsInfo(db dbm.DB, nextHeight, changeHeight int64, params types.ConsensusParams) { + saveConsensusParamsInfo(db, nextHeight, changeHeight, params) +} + +// SaveValidatorsInfo is an alias for the private saveValidatorsInfo method in +// store.go, exported exclusively and explicitly for testing. +func SaveValidatorsInfo(db dbm.DB, height, lastHeightChanged int64, valSet *types.ValidatorSet) { + saveValidatorsInfo(db, height, lastHeightChanged, valSet) +} diff --git a/state/helpers_test.go b/state/helpers_test.go new file mode 100644 index 000000000..e8cb27585 --- /dev/null +++ b/state/helpers_test.go @@ -0,0 +1,280 @@ +package state_test + +import ( + "bytes" + "fmt" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type paramsChangeTestCase struct { + height int64 + params types.ConsensusParams +} + +// always returns true if asked if any evidence was already committed. +type mockEvPoolAlwaysCommitted struct{} + +func (m mockEvPoolAlwaysCommitted) PendingEvidence(int64) []types.Evidence { return nil } +func (m mockEvPoolAlwaysCommitted) AddEvidence(types.Evidence) error { return nil } +func (m mockEvPoolAlwaysCommitted) Update(*types.Block, sm.State) {} +func (m mockEvPoolAlwaysCommitted) IsCommitted(types.Evidence) bool { return true } + +func newTestApp() proxy.AppConns { + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + return proxy.NewAppConns(cc) +} + +func makeAndCommitGoodBlock( + 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) { + // A good block passes + state, blockID, err := makeAndApplyGoodBlock(state, height, lastCommit, proposerAddr, blockExec, evidence) + if err != nil { + return state, types.BlockID{}, nil, err + } + + // Simulate a lastCommit for this block from all validators for the next height + commit, err := makeValidCommit(height, blockID, state.Validators, privVals) + if err != nil { + return state, types.BlockID{}, nil, err + } + 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) { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, evidence, proposerAddr) + if err := blockExec.ValidateBlock(state, block); err != nil { + return state, types.BlockID{}, err + } + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}} + state, err := blockExec.ApplyBlock(state, blockID, block) + if err != nil { + return state, types.BlockID{}, err + } + return state, blockID, nil +} + +func makeVote(height int64, blockID types.BlockID, valSet *types.ValidatorSet, privVal types.PrivValidator) (*types.Vote, error) { + addr := privVal.GetPubKey().Address() + idx, _ := valSet.GetByAddress(addr) + vote := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: height, + Round: 0, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + if err := privVal.SignVote(chainID, vote); err != nil { + return nil, err + } + return vote, nil +} + +func makeValidCommit(height int64, blockID types.BlockID, vals *types.ValidatorSet, privVals map[string]types.PrivValidator) (*types.Commit, error) { + sigs := make([]*types.CommitSig, 0) + for i := 0; i < vals.Size(); i++ { + _, val := vals.GetByIndex(i) + vote, err := makeVote(height, blockID, vals, privVals[val.Address.String()]) + if err != nil { + return nil, err + } + sigs = append(sigs, vote.CommitSig()) + } + return types.NewCommit(blockID, sigs), nil +} + +// make some bogus txs +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < nTxsPerBlock; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeState(nVals, height int) (sm.State, dbm.DB, map[string]types.PrivValidator) { + vals := make([]types.GenesisValidator, nVals) + privVals := make(map[string]types.PrivValidator, nVals) + for i := 0; i < nVals; i++ { + secret := []byte(fmt.Sprintf("test%d", i)) + pk := ed25519.GenPrivKeyFromSecret(secret) + valAddr := pk.PubKey().Address() + vals[i] = types.GenesisValidator{ + Address: valAddr, + PubKey: pk.PubKey(), + Power: 1000, + Name: fmt.Sprintf("test%d", i), + } + privVals[valAddr.String()] = types.NewMockPVWithParams(pk, false, false) + } + s, _ := sm.MakeGenesisState(&types.GenesisDoc{ + ChainID: chainID, + Validators: vals, + AppHash: nil, + }) + + stateDB := dbm.NewMemDB() + sm.SaveState(stateDB, s) + + for i := 1; i < height; i++ { + s.LastBlockHeight++ + s.LastValidators = s.Validators.Copy() + sm.SaveState(stateDB, s) + } + return s, stateDB, privVals +} + +func makeBlock(state sm.State, height int64) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(state.LastBlockHeight), new(types.Commit), nil, state.Validators.GetProposer().Address) + return block +} + +func genValSet(size int) *types.ValidatorSet { + vals := make([]*types.Validator, size) + for i := 0; i < size; i++ { + vals[i] = types.NewValidator(ed25519.GenPrivKey().PubKey(), 10) + } + return types.NewValidatorSet(vals) +} + +func makeConsensusParams( + blockBytes, blockGas int64, + blockTimeIotaMs int64, + evidenceAge int64, +) types.ConsensusParams { + return types.ConsensusParams{ + Block: types.BlockParams{ + MaxBytes: blockBytes, + MaxGas: blockGas, + TimeIotaMs: blockTimeIotaMs, + }, + Evidence: types.EvidenceParams{ + MaxAge: evidenceAge, + }, + } +} + +func makeHeaderPartsResponsesValPubKeyChange(state sm.State, pubkey crypto.PubKey) (types.Header, types.BlockID, *sm.ABCIResponses) { + + block := makeBlock(state, state.LastBlockHeight+1) + abciResponses := &sm.ABCIResponses{ + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, + } + + // If the pubkey is new, remove the old and add the new. + _, val := state.NextValidators.GetByIndex(0) + if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) { + abciResponses.EndBlock = &abci.ResponseEndBlock{ + ValidatorUpdates: []abci.ValidatorUpdate{ + types.TM2PB.NewValidatorUpdate(val.PubKey, 0), + types.TM2PB.NewValidatorUpdate(pubkey, 10), + }, + } + } + + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses +} + +func makeHeaderPartsResponsesValPowerChange(state sm.State, power int64) (types.Header, types.BlockID, *sm.ABCIResponses) { + + block := makeBlock(state, state.LastBlockHeight+1) + abciResponses := &sm.ABCIResponses{ + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, + } + + // If the pubkey is new, remove the old and add the new. + _, val := state.NextValidators.GetByIndex(0) + if val.VotingPower != power { + abciResponses.EndBlock = &abci.ResponseEndBlock{ + ValidatorUpdates: []abci.ValidatorUpdate{ + types.TM2PB.NewValidatorUpdate(val.PubKey, power), + }, + } + } + + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses +} + +func makeHeaderPartsResponsesParams(state sm.State, params types.ConsensusParams) (types.Header, types.BlockID, *sm.ABCIResponses) { + + block := makeBlock(state, state.LastBlockHeight+1) + abciResponses := &sm.ABCIResponses{ + EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(¶ms)}, + } + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses +} + +func randomGenesisDoc() *types.GenesisDoc { + pubkey := ed25519.GenPrivKey().PubKey() + return &types.GenesisDoc{ + GenesisTime: tmtime.Now(), + ChainID: "abc", + Validators: []types.GenesisValidator{ + { + Address: pubkey.Address(), + PubKey: pubkey, + Power: 10, + Name: "myval", + }, + }, + ConsensusParams: types.DefaultConsensusParams(), + } +} + +//---------------------------------------------------------------------------- + +type testApp struct { + abci.BaseApplication + + CommitVotes []abci.VoteInfo + ByzantineValidators []abci.Evidence + ValidatorUpdates []abci.ValidatorUpdate +} + +var _ abci.Application = (*testApp)(nil) + +func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) { + return abci.ResponseInfo{} +} + +func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { + app.CommitVotes = req.LastCommitInfo.Votes + app.ByzantineValidators = req.ByzantineValidators + return abci.ResponseBeginBlock{} +} + +func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { + return abci.ResponseEndBlock{ValidatorUpdates: app.ValidatorUpdates} +} + +func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { + return abci.ResponseDeliverTx{Events: []abci.Event{}} +} + +func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { + return abci.ResponseCheckTx{} +} + +func (app *testApp) Commit() abci.ResponseCommit { + return abci.ResponseCommit{} +} + +func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { + return +} diff --git a/state/main_test.go b/state/main_test.go new file mode 100644 index 000000000..00ecf2686 --- /dev/null +++ b/state/main_test.go @@ -0,0 +1,13 @@ +package state_test + +import ( + "os" + "testing" + + "github.com/tendermint/tendermint/types" +) + +func TestMain(m *testing.M) { + types.RegisterMockEvidencesGlobal() + os.Exit(m.Run()) +} diff --git a/state/services.go b/state/services.go index 98f6afce3..10b389ee7 100644 --- a/state/services.go +++ b/state/services.go @@ -43,7 +43,7 @@ type EvidencePool interface { IsCommitted(types.Evidence) bool } -// MockEvidencePool is an empty implementation of a Mempool, useful for testing. +// MockEvidencePool is an empty implementation of EvidencePool, useful for testing. type MockEvidencePool struct{} func (m MockEvidencePool) PendingEvidence(int64) []types.Evidence { return nil } diff --git a/state/state_test.go b/state/state_test.go index 1ff09ccdb..a0f7a4a2a 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -1,4 +1,4 @@ -package state +package state_test import ( "bytes" @@ -10,23 +10,22 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" + sm "github.com/tendermint/tendermint/state" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/types" ) // setupTestCase does setup common to all test cases. -func setupTestCase(t *testing.T) (func(t *testing.T), dbm.DB, State) { +func setupTestCase(t *testing.T) (func(t *testing.T), dbm.DB, sm.State) { config := cfg.ResetTestRoot("state_") dbType := dbm.DBBackendType(config.DBBackend) stateDB := dbm.NewDB("state", dbType, config.DBDir()) - state, err := LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) + state, err := sm.LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) assert.NoError(t, err, "expected no error on LoadStateFromDBOrGenesisFile") tearDown := func(t *testing.T) { os.RemoveAll(config.RootDir) } @@ -59,7 +58,7 @@ func TestMakeGenesisStateNilValidators(t *testing.T) { Validators: nil, } require.Nil(t, doc.ValidateAndComplete()) - state, err := MakeGenesisState(&doc) + state, err := sm.MakeGenesisState(&doc) require.Nil(t, err) require.Equal(t, 0, len(state.Validators.Validators)) require.Equal(t, 0, len(state.NextValidators.Validators)) @@ -73,9 +72,9 @@ func TestStateSaveLoad(t *testing.T) { assert := assert.New(t) state.LastBlockHeight++ - SaveState(stateDB, state) + sm.SaveState(stateDB, state) - loadedState := LoadState(stateDB) + loadedState := sm.LoadState(stateDB) assert.True(state.Equals(loadedState), fmt.Sprintf("expected state and its copy to be identical.\ngot: %v\nexpected: %v\n", loadedState, state)) @@ -92,15 +91,15 @@ func TestABCIResponsesSaveLoad1(t *testing.T) { // Build mock responses. block := makeBlock(state, 2) - abciResponses := NewABCIResponses(block) + abciResponses := sm.NewABCIResponses(block) abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Events: nil} abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Events: nil} abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{ types.TM2PB.NewValidatorUpdate(ed25519.GenPrivKey().PubKey(), 10), }} - saveABCIResponses(stateDB, block.Height, abciResponses) - loadedABCIResponses, err := LoadABCIResponses(stateDB, block.Height) + sm.SaveABCIResponses(stateDB, block.Height, abciResponses) + loadedABCIResponses, err := sm.LoadABCIResponses(stateDB, block.Height) assert.Nil(err) assert.Equal(abciResponses, loadedABCIResponses, fmt.Sprintf("ABCIResponses don't match:\ngot: %v\nexpected: %v\n", @@ -155,24 +154,24 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { // Query all before, this should return error. for i := range cases { h := int64(i + 1) - res, err := LoadABCIResponses(stateDB, h) + res, err := sm.LoadABCIResponses(stateDB, h) assert.Error(err, "%d: %#v", i, res) } // Add all cases. for i, tc := range cases { h := int64(i + 1) // last block height, one below what we save - responses := &ABCIResponses{ + responses := &sm.ABCIResponses{ DeliverTx: tc.added, EndBlock: &abci.ResponseEndBlock{}, } - saveABCIResponses(stateDB, h, responses) + sm.SaveABCIResponses(stateDB, h, responses) } // Query all before, should return expected value. for i, tc := range cases { h := int64(i + 1) - res, err := LoadABCIResponses(stateDB, h) + res, err := sm.LoadABCIResponses(stateDB, h) assert.NoError(err, "%d", i) assert.Equal(tc.expected.Hash(), res.ResultsHash(), "%d", i) } @@ -186,26 +185,26 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { assert := assert.New(t) // Can't load anything for height 0. - v, err := LoadValidators(stateDB, 0) - assert.IsType(ErrNoValSetForHeight{}, err, "expected err at height 0") + v, err := sm.LoadValidators(stateDB, 0) + assert.IsType(sm.ErrNoValSetForHeight{}, err, "expected err at height 0") // Should be able to load for height 1. - v, err = LoadValidators(stateDB, 1) + v, err = sm.LoadValidators(stateDB, 1) assert.Nil(err, "expected no err at height 1") assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match") // Should be able to load for height 2. - v, err = LoadValidators(stateDB, 2) + v, err = sm.LoadValidators(stateDB, 2) assert.Nil(err, "expected no err at height 2") assert.Equal(v.Hash(), state.NextValidators.Hash(), "expected validator hashes to match") // Increment height, save; should be able to load for next & next next height. state.LastBlockHeight++ nextHeight := state.LastBlockHeight + 1 - saveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) - vp0, err := LoadValidators(stateDB, nextHeight+0) + sm.SaveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) + vp0, err := sm.LoadValidators(stateDB, nextHeight+0) assert.Nil(err, "expected no err") - vp1, err := LoadValidators(stateDB, nextHeight+1) + vp1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(err, "expected no err") assert.Equal(vp0.Hash(), state.Validators.Hash(), "expected validator hashes to match") assert.Equal(vp1.Hash(), state.NextValidators.Hash(), "expected next validator hashes to match") @@ -234,13 +233,13 @@ func TestOneValidatorChangesSaveLoad(t *testing.T) { changeIndex++ power++ } - header, blockID, responses := makeHeaderPartsResponsesValPowerChange(state, i, power) + header, blockID, responses := makeHeaderPartsResponsesValPowerChange(state, power) validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates) require.NoError(t, err) - state, err = updateState(state, blockID, &header, responses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates) require.NoError(t, err) nextHeight := state.LastBlockHeight + 1 - saveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) + sm.SaveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) } // On each height change, increment the power by one. @@ -258,7 +257,7 @@ func TestOneValidatorChangesSaveLoad(t *testing.T) { } for i, power := range testCases { - v, err := LoadValidators(stateDB, int64(i+1+1)) // +1 because vset changes delayed by 1 block. + v, err := sm.LoadValidators(stateDB, int64(i+1+1)) // +1 because vset changes delayed by 1 block. assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", i)) assert.Equal(t, v.Size(), 1, "validator set size is greater than 1: %d", v.Size()) _, val := v.GetByIndex(0) @@ -405,12 +404,12 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { block := makeBlock(state, state.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err := updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) curTotal := val1VotingPower // one increment step and one validator: 0 + power - total_power == 0 @@ -422,7 +421,7 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { updateAddVal := abci.ValidatorUpdate{PubKey: types.TM2PB.PubKey(val2PubKey), Power: val2VotingPower} validatorUpdates, err = types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{updateAddVal}) assert.NoError(t, err) - updatedState2, err := updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState2, err := sm.UpdateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) require.Equal(t, len(updatedState2.NextValidators.Validators), 2) @@ -461,7 +460,7 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { // this will cause the diff of priorities (77) // to be larger than threshold == 2*totalVotingPower (22): - updatedState3, err := updateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState3, err := sm.UpdateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) require.Equal(t, len(updatedState3.NextValidators.Validators), 2) @@ -516,13 +515,13 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { block := makeBlock(state, state.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err := updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) // 0 + 10 (initial prio) - 10 (avg) - 10 (mostest - total) = -10 @@ -537,7 +536,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { validatorUpdates, err = types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{updateAddVal}) assert.NoError(t, err) - updatedState2, err := updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState2, err := sm.UpdateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) require.Equal(t, len(updatedState2.NextValidators.Validators), 2) @@ -574,7 +573,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState3, err := updateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState3, err := sm.UpdateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) assert.Equal(t, updatedState3.Validators.Proposer.Address, updatedState3.NextValidators.Proposer.Address) @@ -598,13 +597,13 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { // no changes in voting power and both validators have same voting power // -> proposers should alternate: oldState := updatedState3 - abciResponses = &ABCIResponses{ + abciResponses = &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - oldState, err = updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + oldState, err = sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) expectedVal1Prio2 = 1 expectedVal2Prio2 = -1 @@ -613,13 +612,13 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { for i := 0; i < 1000; i++ { // no validator updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) // alternate (and cyclic priorities): assert.NotEqual(t, updatedState.Validators.Proposer.Address, updatedState.NextValidators.Proposer.Address, "iter: %v", i) @@ -660,7 +659,7 @@ func TestLargeGenesisValidator(t *testing.T) { oldState := state for i := 0; i < 10; i++ { // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) @@ -669,7 +668,7 @@ func TestLargeGenesisValidator(t *testing.T) { block := makeBlock(oldState, oldState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) // no changes in voting power (ProposerPrio += VotingPower == Voting in 1st round; than shiftByAvg == 0, // than -Total == -Voting) @@ -689,18 +688,18 @@ func TestLargeGenesisValidator(t *testing.T) { firstAddedVal := abci.ValidatorUpdate{PubKey: types.TM2PB.PubKey(firstAddedValPubKey), Power: firstAddedValVotingPower} validatorUpdates, err := types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{firstAddedVal}) assert.NoError(t, err) - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) lastState := updatedState for i := 0; i < 200; i++ { // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) @@ -709,7 +708,7 @@ func TestLargeGenesisValidator(t *testing.T) { block := makeBlock(lastState, lastState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedStateInner, err := updateState(lastState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedStateInner, err := sm.UpdateState(lastState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) lastState = updatedStateInner } @@ -734,26 +733,26 @@ func TestLargeGenesisValidator(t *testing.T) { validatorUpdates, err := types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{addedVal}) assert.NoError(t, err) - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{addedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - state, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) } require.Equal(t, 10+2, len(state.NextValidators.Validators)) // remove genesis validator: removeGenesisVal := abci.ValidatorUpdate{PubKey: types.TM2PB.PubKey(genesisPubKey), Power: 0} - abciResponses = &ABCIResponses{ + abciResponses = &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal}}, } block = makeBlock(oldState, oldState.LastBlockHeight+1) blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err = sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) // only the first added val (not the genesis val) should be left assert.Equal(t, 11, len(updatedState.NextValidators.Validators)) @@ -764,14 +763,14 @@ func TestLargeGenesisValidator(t *testing.T) { count := 0 isProposerUnchanged := true for isProposerUnchanged { - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) block = makeBlock(curState, curState.LastBlockHeight+1) blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - curState, err = updateState(curState, blockID, &block.Header, abciResponses, validatorUpdates) + curState, err = sm.UpdateState(curState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) if !bytes.Equal(curState.Validators.Proposer.Address, curState.NextValidators.Proposer.Address) { isProposerUnchanged = false @@ -787,7 +786,7 @@ func TestLargeGenesisValidator(t *testing.T) { proposers := make([]*types.Validator, numVals) for i := 0; i < 100; i++ { // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) @@ -796,7 +795,7 @@ func TestLargeGenesisValidator(t *testing.T) { block := makeBlock(updatedState, updatedState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedState, err = updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err = sm.UpdateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) if i > numVals { // expect proposers to cycle through after the first iteration (of numVals blocks): if proposers[i%numVals] == nil { @@ -814,15 +813,15 @@ func TestStoreLoadValidatorsIncrementsProposerPriority(t *testing.T) { defer tearDown(t) state.Validators = genValSet(valSetSize) state.NextValidators = state.Validators.CopyIncrementProposerPriority(1) - SaveState(stateDB, state) + sm.SaveState(stateDB, state) nextHeight := state.LastBlockHeight + 1 - v0, err := LoadValidators(stateDB, nextHeight) + v0, err := sm.LoadValidators(stateDB, nextHeight) assert.Nil(t, err) acc0 := v0.Validators[0].ProposerPriority - v1, err := LoadValidators(stateDB, nextHeight+1) + v1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(t, err) acc1 := v1.Validators[0].ProposerPriority @@ -838,28 +837,27 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { require.Equal(t, int64(0), state.LastBlockHeight) state.Validators = genValSet(valSetSize) state.NextValidators = state.Validators.CopyIncrementProposerPriority(1) - SaveState(stateDB, state) + sm.SaveState(stateDB, state) _, valOld := state.Validators.GetByIndex(0) var pubkeyOld = valOld.PubKey pubkey := ed25519.GenPrivKey().PubKey() - const height = 1 // Swap the first validator with a new one (validator set size stays the same). - header, blockID, responses := makeHeaderPartsResponsesValPubKeyChange(state, height, pubkey) + header, blockID, responses := makeHeaderPartsResponsesValPubKeyChange(state, pubkey) // Save state etc. var err error var validatorUpdates []*types.Validator validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates) require.NoError(t, err) - state, err = updateState(state, blockID, &header, responses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates) require.Nil(t, err) nextHeight := state.LastBlockHeight + 1 - saveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) + sm.SaveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) // Load nextheight, it should be the oldpubkey. - v0, err := LoadValidators(stateDB, nextHeight) + v0, err := sm.LoadValidators(stateDB, nextHeight) assert.Nil(t, err) assert.Equal(t, valSetSize, v0.Size()) index, val := v0.GetByAddress(pubkeyOld.Address()) @@ -869,7 +867,7 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { } // Load nextheight+1, it should be the new pubkey. - v1, err := LoadValidators(stateDB, nextHeight+1) + v1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(t, err) assert.Equal(t, valSetSize, v1.Size()) index, val = v1.GetByAddress(pubkey.Address()) @@ -879,14 +877,6 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { } } -func genValSet(size int) *types.ValidatorSet { - vals := make([]*types.Validator, size) - for i := 0; i < size; i++ { - vals[i] = types.NewValidator(ed25519.GenPrivKey().PubKey(), 10) - } - return types.NewValidatorSet(vals) -} - func TestStateMakeBlock(t *testing.T) { tearDown, _, state := setupTestCase(t) defer tearDown(t) @@ -932,14 +922,14 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { changeIndex++ cp = params[changeIndex] } - header, blockID, responses := makeHeaderPartsResponsesParams(state, i, cp) + header, blockID, responses := makeHeaderPartsResponsesParams(state, cp) validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates) require.NoError(t, err) - state, err = updateState(state, blockID, &header, responses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates) require.Nil(t, err) nextHeight := state.LastBlockHeight + 1 - saveConsensusParamsInfo(stateDB, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams) + sm.SaveConsensusParamsInfo(stateDB, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams) } // Make all the test cases by using the same params until after the change. @@ -957,32 +947,15 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { } for _, testCase := range testCases { - p, err := LoadConsensusParams(stateDB, testCase.height) + p, err := sm.LoadConsensusParams(stateDB, testCase.height) assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", testCase.height)) assert.Equal(t, testCase.params, p, fmt.Sprintf(`unexpected consensus params at height %d`, testCase.height)) } } -func makeParams( - blockBytes, blockGas int64, - blockTimeIotaMs int64, - evidenceAge int64, -) types.ConsensusParams { - return types.ConsensusParams{ - Block: types.BlockParams{ - MaxBytes: blockBytes, - MaxGas: blockGas, - TimeIotaMs: blockTimeIotaMs, - }, - Evidence: types.EvidenceParams{ - MaxAge: evidenceAge, - }, - } -} - func TestApplyUpdates(t *testing.T) { - initParams := makeParams(1, 2, 3, 4) + initParams := makeConsensusParams(1, 2, 3, 4) cases := [...]struct { init types.ConsensusParams @@ -998,14 +971,14 @@ func TestApplyUpdates(t *testing.T) { MaxGas: 55, }, }, - makeParams(44, 55, 3, 4)}, + makeConsensusParams(44, 55, 3, 4)}, 3: {initParams, abci.ConsensusParams{ Evidence: &abci.EvidenceParams{ MaxAge: 66, }, }, - makeParams(1, 2, 3, 66)}, + makeConsensusParams(1, 2, 3, 66)}, } for i, tc := range cases { @@ -1013,61 +986,3 @@ func TestApplyUpdates(t *testing.T) { assert.Equal(t, tc.expected, res, "case %d", i) } } - -func makeHeaderPartsResponsesValPubKeyChange(state State, height int64, - pubkey crypto.PubKey) (types.Header, types.BlockID, *ABCIResponses) { - - block := makeBlock(state, state.LastBlockHeight+1) - abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, - } - - // If the pubkey is new, remove the old and add the new. - _, val := state.NextValidators.GetByIndex(0) - if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) { - abciResponses.EndBlock = &abci.ResponseEndBlock{ - ValidatorUpdates: []abci.ValidatorUpdate{ - types.TM2PB.NewValidatorUpdate(val.PubKey, 0), - types.TM2PB.NewValidatorUpdate(pubkey, 10), - }, - } - } - - return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses -} - -func makeHeaderPartsResponsesValPowerChange(state State, height int64, - power int64) (types.Header, types.BlockID, *ABCIResponses) { - - block := makeBlock(state, state.LastBlockHeight+1) - abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, - } - - // If the pubkey is new, remove the old and add the new. - _, val := state.NextValidators.GetByIndex(0) - if val.VotingPower != power { - abciResponses.EndBlock = &abci.ResponseEndBlock{ - ValidatorUpdates: []abci.ValidatorUpdate{ - types.TM2PB.NewValidatorUpdate(val.PubKey, power), - }, - } - } - - return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses -} - -func makeHeaderPartsResponsesParams(state State, height int64, - params types.ConsensusParams) (types.Header, types.BlockID, *ABCIResponses) { - - block := makeBlock(state, state.LastBlockHeight+1) - abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(¶ms)}, - } - return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses -} - -type paramsChangeTestCase struct { - height int64 - params types.ConsensusParams -} diff --git a/state/store_test.go b/state/store_test.go index 06adeefab..0cf217722 100644 --- a/state/store_test.go +++ b/state/store_test.go @@ -1,4 +1,4 @@ -package state +package state_test import ( "fmt" @@ -10,6 +10,7 @@ import ( cfg "github.com/tendermint/tendermint/config" dbm "github.com/tendermint/tendermint/libs/db" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -19,9 +20,9 @@ func TestStoreLoadValidators(t *testing.T) { vals := types.NewValidatorSet([]*types.Validator{val}) // 1) LoadValidators loads validators using a height where they were last changed - saveValidatorsInfo(stateDB, 1, 1, vals) - saveValidatorsInfo(stateDB, 2, 1, vals) - loadedVals, err := LoadValidators(stateDB, 2) + sm.SaveValidatorsInfo(stateDB, 1, 1, vals) + sm.SaveValidatorsInfo(stateDB, 2, 1, vals) + loadedVals, err := sm.LoadValidators(stateDB, 2) require.NoError(t, err) assert.NotZero(t, loadedVals.Size()) @@ -30,13 +31,13 @@ func TestStoreLoadValidators(t *testing.T) { // TODO(melekes): REMOVE in 0.33 release // https://github.com/tendermint/tendermint/issues/3543 // for releases prior to v0.31.4, it uses last height changed - valInfo := &ValidatorsInfo{ - LastHeightChanged: valSetCheckpointInterval, + valInfo := &sm.ValidatorsInfo{ + LastHeightChanged: sm.ValSetCheckpointInterval, } - stateDB.Set(calcValidatorsKey(valSetCheckpointInterval), valInfo.Bytes()) + stateDB.Set(sm.CalcValidatorsKey(sm.ValSetCheckpointInterval), valInfo.Bytes()) assert.NotPanics(t, func() { - saveValidatorsInfo(stateDB, valSetCheckpointInterval+1, 1, vals) - loadedVals, err := LoadValidators(stateDB, valSetCheckpointInterval+1) + sm.SaveValidatorsInfo(stateDB, sm.ValSetCheckpointInterval+1, 1, vals) + loadedVals, err := sm.LoadValidators(stateDB, sm.ValSetCheckpointInterval+1) if err != nil { t.Fatal(err) } @@ -46,9 +47,9 @@ func TestStoreLoadValidators(t *testing.T) { }) // ENDREMOVE - saveValidatorsInfo(stateDB, valSetCheckpointInterval, 1, vals) + sm.SaveValidatorsInfo(stateDB, sm.ValSetCheckpointInterval, 1, vals) - loadedVals, err = LoadValidators(stateDB, valSetCheckpointInterval) + loadedVals, err = sm.LoadValidators(stateDB, sm.ValSetCheckpointInterval) require.NoError(t, err) assert.NotZero(t, loadedVals.Size()) } @@ -60,20 +61,20 @@ func BenchmarkLoadValidators(b *testing.B) { defer os.RemoveAll(config.RootDir) dbType := dbm.DBBackendType(config.DBBackend) stateDB := dbm.NewDB("state", dbType, config.DBDir()) - state, err := LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) + state, err := sm.LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) if err != nil { b.Fatal(err) } state.Validators = genValSet(valSetSize) state.NextValidators = state.Validators.CopyIncrementProposerPriority(1) - SaveState(stateDB, state) + sm.SaveState(stateDB, state) for i := 10; i < 10000000000; i *= 10 { // 10, 100, 1000, ... - saveValidatorsInfo(stateDB, int64(i), state.LastHeightValidatorsChanged, state.NextValidators) + sm.SaveValidatorsInfo(stateDB, int64(i), state.LastHeightValidatorsChanged, state.NextValidators) b.Run(fmt.Sprintf("height=%d", i), func(b *testing.B) { for n := 0; n < b.N; n++ { - _, err := LoadValidators(stateDB, int64(i)) + _, err := sm.LoadValidators(stateDB, int64(i)) if err != nil { b.Fatal(err) } diff --git a/state/tx_filter_test.go b/state/tx_filter_test.go index e48ad2c3e..bd3243168 100644 --- a/state/tx_filter_test.go +++ b/state/tx_filter_test.go @@ -1,4 +1,4 @@ -package state +package state_test import ( "os" @@ -7,11 +7,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" ) func TestTxFilter(t *testing.T) { @@ -34,10 +33,10 @@ func TestTxFilter(t *testing.T) { for i, tc := range testCases { stateDB := dbm.NewDB("state", "memdb", os.TempDir()) - state, err := LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) require.NoError(t, err) - f := TxPreCheck(state) + f := sm.TxPreCheck(state) if tc.isErr { assert.NotNil(t, f(tc.tx), "#%v", i) } else { @@ -45,13 +44,3 @@ func TestTxFilter(t *testing.T) { } } } - -func randomGenesisDoc() *types.GenesisDoc { - pubkey := ed25519.GenPrivKey().PubKey() - return &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: "abc", - Validators: []types.GenesisValidator{{Address: pubkey.Address(), PubKey: pubkey, Power: 10, Name: "myval"}}, - ConsensusParams: types.DefaultConsensusParams(), - } -} diff --git a/state/validation.go b/state/validation.go index 3c63c35b7..1d365e90c 100644 --- a/state/validation.go +++ b/state/validation.go @@ -94,10 +94,7 @@ func validateBlock(evidencePool EvidencePool, stateDB dbm.DB, state State, block } } else { if len(block.LastCommit.Precommits) != state.LastValidators.Size() { - return fmt.Errorf("Invalid block commit size. Expected %v, got %v", - state.LastValidators.Size(), - len(block.LastCommit.Precommits), - ) + return types.NewErrInvalidCommitPrecommits(state.LastValidators.Size(), len(block.LastCommit.Precommits)) } err := state.LastValidators.VerifyCommit( state.ChainID, state.LastBlockID, block.Height-1, block.LastCommit) diff --git a/state/validation_test.go b/state/validation_test.go index 705f843df..c53cf0102 100644 --- a/state/validation_test.go +++ b/state/validation_test.go @@ -1,31 +1,30 @@ -package state +package state_test import ( "testing" "time" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/libs/log" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" ) -// TODO(#2589): -// - generalize this past the first height -// - add txs and build up full State properly -// - test block.Time (see #2587 - there are no conditions on time for the first height) -func TestValidateBlockHeader(t *testing.T) { - var height int64 = 1 // TODO(#2589): generalize - state, stateDB := state(1, int(height)) +const validationTestsStopHeight int64 = 10 - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, nil) +func TestValidateBlockHeader(t *testing.T) { + proxyApp := newTestApp() + require.NoError(t, proxyApp.Start()) + defer proxyApp.Stop() - // A good block passes. - block := makeBlock(state, height) - err := blockExec.ValidateBlock(state, block) - require.NoError(t, err) + state, stateDB, privVals := makeState(3, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) + lastCommit := types.NewCommit(types.BlockID{}, nil) // some bad values wrongHash := tmhash.Sum([]byte("this hash is wrong")) @@ -43,7 +42,7 @@ func TestValidateBlockHeader(t *testing.T) { {"Version wrong2", func(block *types.Block) { block.Version = wrongVersion2 }}, {"ChainID wrong", func(block *types.Block) { block.ChainID = "not-the-real-one" }}, {"Height wrong", func(block *types.Block) { block.Height += 10 }}, - {"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 3600 * 24) }}, + {"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 1) }}, {"NumTxs wrong", func(block *types.Block) { block.NumTxs += 10 }}, {"TotalTxs wrong", func(block *types.Block) { block.TotalTxs += 10 }}, @@ -62,78 +61,145 @@ func TestValidateBlockHeader(t *testing.T) { {"Proposer invalid", func(block *types.Block) { block.ProposerAddress = []byte("wrong size") }}, } - for _, tc := range testCases { - block := makeBlock(state, height) - tc.malleateBlock(block) - err := blockExec.ValidateBlock(state, block) - require.Error(t, err, tc.name) + // Build up state for multiple heights + for height := int64(1); height < validationTestsStopHeight; height++ { + proposerAddr := state.Validators.GetProposer().Address + /* + Invalid blocks don't pass + */ + for _, tc := range testCases { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, proposerAddr) + tc.malleateBlock(block) + err := blockExec.ValidateBlock(state, block) + require.Error(t, err, tc.name) + } + + /* + A good block passes + */ + var err error + state, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil) + require.NoError(t, err, "height %d", height) } } -/* - TODO(#2589): - - test Block.Data.Hash() == Block.DataHash - - test len(Block.Data.Txs) == Block.NumTxs -*/ -func TestValidateBlockData(t *testing.T) { -} - -/* - TODO(#2589): - - test len(block.LastCommit.Precommits) == state.LastValidators.Size() - - test state.LastValidators.VerifyCommit -*/ func TestValidateBlockCommit(t *testing.T) { + proxyApp := newTestApp() + require.NoError(t, proxyApp.Start()) + defer proxyApp.Stop() + + state, stateDB, privVals := makeState(1, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) + lastCommit := types.NewCommit(types.BlockID{}, nil) + wrongPrecommitsCommit := types.NewCommit(types.BlockID{}, nil) + badPrivVal := types.NewMockPV() + + for height := int64(1); height < validationTestsStopHeight; height++ { + proposerAddr := state.Validators.GetProposer().Address + if height > 1 { + /* + #2589: ensure state.LastValidators.VerifyCommit fails here + */ + // should be height-1 instead of height + wrongHeightVote, err := makeVote(height, state.LastBlockID, state.Validators, privVals[proposerAddr.String()]) + require.NoError(t, err, "height %d", height) + wrongHeightCommit := types.NewCommit(state.LastBlockID, []*types.CommitSig{wrongHeightVote.CommitSig()}) + block, _ := state.MakeBlock(height, makeTxs(height), wrongHeightCommit, nil, proposerAddr) + err = blockExec.ValidateBlock(state, block) + _, isErrInvalidCommitHeight := err.(types.ErrInvalidCommitHeight) + require.True(t, isErrInvalidCommitHeight, "expected ErrInvalidCommitHeight at height %d but got: %v", height, err) + + /* + #2589: test len(block.LastCommit.Precommits) == state.LastValidators.Size() + */ + block, _ = state.MakeBlock(height, makeTxs(height), wrongPrecommitsCommit, nil, proposerAddr) + err = blockExec.ValidateBlock(state, block) + _, isErrInvalidCommitPrecommits := err.(types.ErrInvalidCommitPrecommits) + require.True(t, isErrInvalidCommitPrecommits, "expected ErrInvalidCommitPrecommits at height %d but got: %v", height, err) + } + + /* + A good block passes + */ + var err error + var blockID types.BlockID + state, blockID, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil) + require.NoError(t, err, "height %d", height) + + /* + wrongPrecommitsCommit is fine except for the extra bad precommit + */ + goodVote, err := makeVote(height, blockID, state.Validators, privVals[proposerAddr.String()]) + require.NoError(t, err, "height %d", height) + badVote := &types.Vote{ + ValidatorAddress: badPrivVal.GetPubKey().Address(), + ValidatorIndex: 0, + Height: height, + Round: 0, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + err = badPrivVal.SignVote(chainID, goodVote) + require.NoError(t, err, "height %d", height) + wrongPrecommitsCommit = types.NewCommit(blockID, []*types.CommitSig{goodVote.CommitSig(), badVote.CommitSig()}) + } } -/* - TODO(#2589): - - test good/bad evidence in block -*/ func TestValidateBlockEvidence(t *testing.T) { - var height int64 = 1 // TODO(#2589): generalize - state, stateDB := state(1, int(height)) - - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, nil) - - // make some evidence - addr, _ := state.Validators.GetByIndex(0) - goodEvidence := types.NewMockGoodEvidence(height, 0, addr) - - // A block with a couple pieces of evidence passes. - block := makeBlock(state, height) - block.Evidence.Evidence = []types.Evidence{goodEvidence, goodEvidence} - block.EvidenceHash = block.Evidence.Hash() - err := blockExec.ValidateBlock(state, block) - require.NoError(t, err) - - // A block with too much evidence fails. - maxBlockSize := state.ConsensusParams.Block.MaxBytes - maxNumEvidence, _ := types.MaxEvidencePerBlock(maxBlockSize) - require.True(t, maxNumEvidence > 2) - for i := int64(0); i < maxNumEvidence; i++ { - block.Evidence.Evidence = append(block.Evidence.Evidence, goodEvidence) + proxyApp := newTestApp() + require.NoError(t, proxyApp.Start()) + defer proxyApp.Stop() + + state, stateDB, privVals := makeState(3, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) + lastCommit := types.NewCommit(types.BlockID{}, nil) + + for height := int64(1); height < validationTestsStopHeight; height++ { + proposerAddr := state.Validators.GetProposer().Address + proposerIdx, _ := state.Validators.GetByAddress(proposerAddr) + goodEvidence := types.NewMockGoodEvidence(height, proposerIdx, proposerAddr) + if height > 1 { + /* + A block with too much evidence fails + */ + maxBlockSize := state.ConsensusParams.Block.MaxBytes + maxNumEvidence, _ := types.MaxEvidencePerBlock(maxBlockSize) + require.True(t, maxNumEvidence > 2) + evidence := make([]types.Evidence, 0) + // one more than the maximum allowed evidence + for i := int64(0); i <= maxNumEvidence; i++ { + evidence = append(evidence, goodEvidence) + } + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, evidence, proposerAddr) + err := blockExec.ValidateBlock(state, block) + _, ok := err.(*types.ErrEvidenceOverflow) + require.True(t, ok, "expected error to be of type ErrEvidenceOverflow at height %d", height) + } + + /* + A good block with several pieces of good evidence passes + */ + maxBlockSize := state.ConsensusParams.Block.MaxBytes + maxNumEvidence, _ := types.MaxEvidencePerBlock(maxBlockSize) + require.True(t, maxNumEvidence > 2) + evidence := make([]types.Evidence, 0) + // precisely the amount of allowed evidence + for i := int64(0); i < maxNumEvidence; i++ { + evidence = append(evidence, goodEvidence) + } + + var err error + state, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, evidence) + require.NoError(t, err, "height %d", height) } - block.EvidenceHash = block.Evidence.Hash() - err = blockExec.ValidateBlock(state, block) - require.Error(t, err) - _, ok := err.(*types.ErrEvidenceOverflow) - require.True(t, ok) } -// always returns true if asked if any evidence was already committed. -type mockEvPoolAlwaysCommitted struct{} - -func (m mockEvPoolAlwaysCommitted) PendingEvidence(int64) []types.Evidence { return nil } -func (m mockEvPoolAlwaysCommitted) AddEvidence(types.Evidence) error { return nil } -func (m mockEvPoolAlwaysCommitted) Update(*types.Block, State) {} -func (m mockEvPoolAlwaysCommitted) IsCommitted(types.Evidence) bool { return true } - func TestValidateFailBlockOnCommittedEvidence(t *testing.T) { var height int64 = 1 - state, stateDB := state(1, int(height)) + state, stateDB, _ := makeState(1, int(height)) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, mockEvPoolAlwaysCommitted{}) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, mockEvPoolAlwaysCommitted{}) // A block with a couple pieces of evidence passes. block := makeBlock(state, height) addr, _ := state.Validators.GetByIndex(0) @@ -145,12 +211,3 @@ func TestValidateFailBlockOnCommittedEvidence(t *testing.T) { require.Error(t, err) require.IsType(t, err, &types.ErrEvidenceInvalid{}) } - -/* - TODO(#2589): - - test unmarshalling BlockParts that are too big into a Block that - (note this logic happens in the consensus, not in the validation here). - - test making blocks from the types.MaxXXX functions works/fails as expected -*/ -func TestValidateBlockSize(t *testing.T) { -} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 000000000..603ac51d7 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,41 @@ +package types + +import "fmt" + +type ( + // ErrInvalidCommitHeight is returned when we encounter a commit with an + // unexpected height. + ErrInvalidCommitHeight struct { + Expected int64 + Actual int64 + } + + // ErrInvalidCommitPrecommits is returned when we encounter a commit where + // the number of precommits doesn't match the number of validators. + ErrInvalidCommitPrecommits struct { + Expected int + Actual int + } +) + +func NewErrInvalidCommitHeight(expected, actual int64) ErrInvalidCommitHeight { + return ErrInvalidCommitHeight{ + Expected: expected, + Actual: actual, + } +} + +func (e ErrInvalidCommitHeight) Error() string { + return fmt.Sprintf("Invalid commit -- wrong height: %v vs %v", e.Expected, e.Actual) +} + +func NewErrInvalidCommitPrecommits(expected, actual int) ErrInvalidCommitPrecommits { + return ErrInvalidCommitPrecommits{ + Expected: expected, + Actual: actual, + } +} + +func (e ErrInvalidCommitPrecommits) Error() string { + return fmt.Sprintf("Invalid commit -- wrong set size: %v vs %v", e.Expected, e.Actual) +} diff --git a/types/validator_set.go b/types/validator_set.go index 9e78fbc77..65358714d 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -594,10 +594,10 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i return err } if vals.Size() != len(commit.Precommits) { - return fmt.Errorf("Invalid commit -- wrong set size: %v vs %v", vals.Size(), len(commit.Precommits)) + return NewErrInvalidCommitPrecommits(vals.Size(), len(commit.Precommits)) } if height != commit.Height() { - return fmt.Errorf("Invalid commit -- wrong height: %v vs %v", height, commit.Height()) + return NewErrInvalidCommitHeight(height, commit.Height()) } if !blockID.Equals(commit.BlockID) { return fmt.Errorf("Invalid commit -- wrong block id: want %v got %v", From 8fc83684384423eafbf2a3f421de3ff09216c0bc Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 22 Jun 2019 10:30:23 +0400 Subject: [PATCH 072/211] node: run whole func in goroutine, not just logger.Error fn (#3743) * node: run whole func in goroutine, not just logger.Error fn Fixes #3741 * add a changelog entry --- CHANGELOG_PENDING.md | 1 + node/node.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 74aa72c6c..1bff64b64 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -40,3 +40,4 @@ - [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) - [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) - [node] \#3716 Fix a bug where `nil` is recorded as node's address +- [node] \#3741 Fix profiler blocking the entire node diff --git a/node/node.go b/node/node.go index 85fef5ee7..c992e2424 100644 --- a/node/node.go +++ b/node/node.go @@ -630,7 +630,9 @@ func NewNode(config *cfg.Config, } if config.ProfListenAddress != "" { - go logger.Error("Profile server", "err", http.ListenAndServe(config.ProfListenAddress, nil)) + go func() { + logger.Error("Profile server", "err", http.ListenAndServe(config.ProfListenAddress, nil)) + }() } node := &Node{ From 1b77bf6f209fae35cb4693123572263f76c0a0bf Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 22 Jun 2019 19:44:12 +0400 Subject: [PATCH 073/211] rpc/lib: write a test for TLS server (#3703) * rpc/lib: write a test for TLS server Refs #3700 * do not regenerate certificates * add nolint --- Makefile | 27 ++++++++++------ libs/db/remotedb/test.crt | 40 ++++++++++++++---------- libs/db/remotedb/test.key | 50 +++++++++++++++--------------- libs/test.sh | 6 ---- rpc/lib/server/http_server_test.go | 33 +++++++++++++------- rpc/lib/server/test.crt | 25 +++++++++++++++ rpc/lib/server/test.key | 27 ++++++++++++++++ 7 files changed, 139 insertions(+), 69 deletions(-) create mode 100644 rpc/lib/server/test.crt create mode 100644 rpc/lib/server/test.key diff --git a/Makefile b/Makefile index 1980ac861..3dff35b9c 100644 --- a/Makefile +++ b/Makefile @@ -115,24 +115,31 @@ get_deps_bin_size: protoc_libs: libs/common/types.pb.go +# generates certificates for TLS testing in remotedb and RPC server gen_certs: clean_certs - ## Generating certificates for TLS testing... certstrap init --common-name "tendermint.com" --passphrase "" - certstrap request-cert -ip "::" --passphrase "" - certstrap sign "::" --CA "tendermint.com" --passphrase "" - mv out/::.crt out/::.key db/remotedb + certstrap request-cert --common-name "remotedb" -ip "127.0.0.1" --passphrase "" + certstrap sign "remotedb" --CA "tendermint.com" --passphrase "" + mv out/remotedb.crt libs/db/remotedb/test.crt + mv out/remotedb.key libs/db/remotedb/test.key + certstrap request-cert --common-name "server" -ip "127.0.0.1" --passphrase "" + certstrap sign "server" --CA "tendermint.com" --passphrase "" + mv out/server.crt rpc/lib/server/test.crt + mv out/server.key rpc/lib/server/test.key + rm -rf out +# deletes generated certificates clean_certs: - ## Cleaning TLS testing certificates... - rm -rf out - rm -f db/remotedb/::.crt db/remotedb/::.key + rm -f libs/db/remotedb/test.crt + rm -f libs/db/remotedb/test.key + rm -f rpc/lib/server/test.crt + rm -f rpc/lib/server/test.key -test_libs: gen_certs +test_libs: go test -tags clevedb boltdb $(PACKAGES) - make clean_certs grpc_dbserver: - protoc -I db/remotedb/proto/ db/remotedb/proto/defs.proto --go_out=plugins=grpc:db/remotedb/proto + protoc -I libs/db/remotedb/proto/ libs/db/remotedb/proto/defs.proto --go_out=plugins=grpc:libs/db/remotedb/proto protoc_grpc: rpc/grpc/types.pb.go diff --git a/libs/db/remotedb/test.crt b/libs/db/remotedb/test.crt index 06ffec1d2..1090e73d7 100644 --- a/libs/db/remotedb/test.crt +++ b/libs/db/remotedb/test.crt @@ -1,19 +1,25 @@ -----BEGIN CERTIFICATE----- -MIIDAjCCAeqgAwIBAgIJAOGCVedOwRbOMA0GCSqGSIb3DQEBBQUAMCExCzAJBgNV -BAYTAlVTMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTkwMjExMTU0NjQ5WhcNMjAw -MjExMTU0NjQ5WjAhMQswCQYDVQQGEwJVUzESMBAGA1UEAwwJbG9jYWxob3N0MIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA60S/fNUWoHm1PYI/yrlnZNtr -dRqDORHe0hPwl/lttLz7+a7HzQZFnpiXnuxbDJtpIq/h1vhAl0sFy86Ip26LhbWc -GjxJL24tVwiOwqYRzTPZ/rK3JYuNcIvcztXjMqdzPrHSZy5YZgrQB6yhTiqpBc4D -h/XgWjEt4DhpHwf/zuIK9XkJw0IaTWjFmoyKRoWW3q4bHzoKNxS9bXP117Tz7tn0 -AdsQCjt1GKcIROkcOGUHqByINJ2XlBkb7SQPjQVBLDVJKdRDUt+yHkkdbn97UDhq -HRTCt5UELWs/53Gj1ffNuhjECOVjG1HkZweLgZjJRQYe8X2OOLNOyfVY1KsDnQID -AQABoz0wOzAMBgNVHRMEBTADAQH/MCsGA1UdEQQkMCKCCWxvY2FsaG9zdIIJbG9j -YWxob3N0hwQAAAAAhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQCe2A5gDc3jiZwT -a5TJrc2J2KouqxB/PCddw5VY8jPsZJfsr9gxHi+Xa5g8p3oqmEOIlqM5BVhrZRUG -RWHDmL+bCsuzMoA/vGHtHmUIwLeZQLWgT3kv12Dc8M9flNNjmXWxdMR9lOMwcL83 -F0CdElxSmaEbNvCIJBDetJJ7vMCqS2lnTLWurbH4ZGeGwvjzNgpgGCKwbyK/gU+j -UXiTQbVvPQ3WWACDnfH6rg0TpxU9jOBkd+4/9tUrBG7UclQBfGULk3sObLO9kx4N -8RxJmtp8jljIXVPX3udExI05pz039pAgvaeZWtP17QSbYcKF1jFtKo6ckrv2GKXX -M5OXGXdw +MIIEOjCCAiKgAwIBAgIQYO+jRR0Sbs+WzU/hj2aoxzANBgkqhkiG9w0BAQsFADAZ +MRcwFQYDVQQDEw50ZW5kZXJtaW50LmNvbTAeFw0xOTA2MDIxMTAyMDdaFw0yMDEy +MDIxMTAyMDRaMBMxETAPBgNVBAMTCHJlbW90ZWRiMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAt7YkYMJ5X5X3MT1tWG1KFO3uyZl962fInl+43xVESydp +qYYHYei7b3T8c/3Ww6f3aKkkCHrvPtqHZjU6o+wp/AQMNlyUoyRN89+6Oj67u2C7 +iZjzAJ+Pk87jMaStubvmZ9J+uk4op4rv5Rt4ns/Kg70RaMvqYR8tGqPcy3o8fWS+ +hCbuwAS8b65yp+AgbnThDEBUnieN3OFLfDV//45qw2OlqlM/gHOVT2JMRbl14Y7x +tW3/Xe+lsB7B3+OC6NQ2Nu7DEA1X+TBNyItIGnQH6DwK2ZBRtyQEk26FAWVj8fHd +A5I4+RcGWXz4T6gJmDZN7+47WHO0ProjARbUV0GIuQIDAQABo4GDMIGAMA4GA1Ud +DwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0O +BBYEFOA8wzCYhoZmy0WHgnv/0efijUMKMB8GA1UdIwQYMBaAFNSTPe743aIx7rIp +vn5HV3gJ4z1hMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAKZf +EVo0i9nMZv6ZJjbmAlMfo5FH41/oBYC8pyGAnJKl42raXKJAbl45h80iGn3vNggf +7HJjN+znAHDFYjIwK2IV2WhHPyxK6uk+FA5uBR/aAPcw+zhRfXUMYdhNHr6KBlZZ +bvD7Iq4UALg+XFQz/fQkIi7QvTBwkYyPNA2+a/TGf6myMp26hoz73DQXklqm6Zle +myPs1Vp9bTgOv/3l64BMUV37FZ2TyiisBkV1qPEoDxT7Fbi8G1K8gMDLd0wu0jvX +nz96nk30TDnZewV1fhkMJVKKGiLbaIgHcu1lWsWJZ0tdc+MF7R9bLBO5T0cTDgNy +V8/51g+Cxu5SSHKjFkT0vBBONhjPmRqzJpxOQfHjiv8mmHwwiaNNy2VkJHj5GHer +64r67fQTSqAifzgwAbXYK+ObUbx4PnHvSYSF5dbcR1Oj6UTVtGAgdmN2Y03AIc1B +CiaojcMVuMRz/SvmPWl34GBvvT5/h9VCpHEB3vV6bQxJb5U1fLyo4GABA2Ic3DHr +kV5p7CZI06UNbyQyFtnEb5XoXywRa4Df7FzDIv3HL13MtyXrYrJqC1eAbn+3jGdh +bQa510mWYAlQQmzHSf/SLKott4QKR3SmhOGqGKNvquAYJ9XLdYdsPmKKGH6iGUD8 +n7yEi0KMD/BHsPQNNLatsR2SxqGDeLhbLR0w2hig -----END CERTIFICATE----- diff --git a/libs/db/remotedb/test.key b/libs/db/remotedb/test.key index e1adb3e1e..b30bf809a 100644 --- a/libs/db/remotedb/test.key +++ b/libs/db/remotedb/test.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/libs/test.sh b/libs/test.sh index 64898b0d2..d0618768b 100755 --- a/libs/test.sh +++ b/libs/test.sh @@ -4,9 +4,6 @@ set -e # run the linter # make lint -# setup certs -make gen_certs - # run the unit tests with coverage echo "" > coverage.txt for d in $(go list ./... | grep -v vendor); do @@ -16,6 +13,3 @@ for d in $(go list ./... | grep -v vendor); do rm profile.out fi done - -# cleanup certs -make clean_certs diff --git a/rpc/lib/server/http_server_test.go b/rpc/lib/server/http_server_test.go index 7f47a30b3..b463aa6a8 100644 --- a/rpc/lib/server/http_server_test.go +++ b/rpc/lib/server/http_server_test.go @@ -1,16 +1,18 @@ package rpcserver import ( + "crypto/tls" "fmt" "io" "io/ioutil" + "net" "net/http" - "os" "sync" "sync/atomic" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/log" @@ -66,18 +68,27 @@ func TestMaxOpenConnections(t *testing.T) { } func TestStartHTTPAndTLSServer(t *testing.T) { - config := DefaultConfig() - config.MaxOpenConnections = 1 - // set up fixtures - listenerAddr := "tcp://0.0.0.0:0" - listener, err := Listen(listenerAddr, config) + ln, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) + defer ln.Close() + mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "some body") + }) - // test failure - err = StartHTTPAndTLSServer(listener, mux, "", "", log.TestingLogger(), config) - require.IsType(t, (*os.PathError)(nil), err) + go StartHTTPAndTLSServer(ln, mux, "test.crt", "test.key", log.TestingLogger(), DefaultConfig()) - // TODO: test that starting the server can actually work + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint: gosec + } + c := &http.Client{Transport: tr} + res, err := c.Get("https://" + ln.Addr().String()) + require.NoError(t, err) + defer res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + + body, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, []byte("some body"), body) } diff --git a/rpc/lib/server/test.crt b/rpc/lib/server/test.crt new file mode 100644 index 000000000..e4ab1965d --- /dev/null +++ b/rpc/lib/server/test.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEODCCAiCgAwIBAgIQWDHUrd4tOM2xExWhzOEJ7DANBgkqhkiG9w0BAQsFADAZ +MRcwFQYDVQQDEw50ZW5kZXJtaW50LmNvbTAeFw0xOTA2MDIxMTAyMDdaFw0yMDEy +MDIxMTAyMDRaMBExDzANBgNVBAMTBnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANBaa6dc9GZcIhAHWqVrx0LONYf+IlbvTP7yrV45ws0ix8TX +1NUOiDY1cwzKH8ay/HYX45e2fFLrtLidc9h+apsC55k3Vdcy00+Ksr/adjR8D4A/ +GpnTS+hVDHTlqINe9a7USok34Zr1rc3fh4Imu5RxEurjMwkA/36k6+OpXMp2qlKY +S1fGqwn2KGhXkp/yTWZILEMXBazNxGx4xfqYXzWm6boeyJAXpM2DNkv7dtwa/CWY +WacUQJApNInwn5+B8LLoo+pappkfZOjAD9/aHKsyFTSWmmWeg7V//ouB3u5vItqf +GP+3xmPgeYeEyOIe/P2f8bRuQs+GGwSCmi6F1GUCAwEAAaOBgzCBgDAOBgNVHQ8B +Af8EBAMCA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW +BBSpBFIMbkBR4xVYQZtUJQQwzPmbHjAfBgNVHSMEGDAWgBTUkz3u+N2iMe6yKb5+ +R1d4CeM9YTAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQBCqdzS +tPHkMYWjYs6aREwob9whjyG8a4Qp6IkP1SYHCwpzsTeWLi9ybEcDRb3jZ4iRxbZg +7GFxjqHoWgBZHAIyICMsHupOJEtXq5hx86NuMwk/12bx1eNj0yTIAnVOA+em/ZtB +zR38OwB8xXmjKd0Ow1Y7zCh5zE2gU+sR0JOJSfxXUZrJvwDNrbcmZPQ+kwuq4cyv +fxZnvZf/owbyOLQFdbiPQbbiZ7JSv8q7GCMleULCEygrsWClYkULUByhKykCHJIU +wfq1owge9EqG/4CDCCjB9vBFmUyv3FJhgWnzd6tPQckFoHSoD0Bjsv/pQFcsGLcg ++e/Mm6hZgCXXwI2WHYbxqz5ToOaRQQYo6N77jWejOBMecOZmPDyQ2nz73aJd11GW +NiDT7pyMlBJA8W4wAvVP4ow2ugqsPjqZ6EyismIGFUTqMp+NtXOsLPK+sEMhKhJ9 +ulczRpPEf25roBt6aEk2fTAfAPmbpvNamBLSbBU23mzJ38RmfhxLOlOgCGbBBX4d +kE+/+En8UJO4X8CKaKRo/c5G2UZ6++2cjp6SPrsGENDMW5yBGegrDw+ow8/bLxIr +OjWpSe2cygovy3aHE6UBOgkxw9KIaSEqFgjQZ0i+xO6l6qQoljQgUGXfecVMR+7C +4KsyVVTMlK9/thA7Zfc8a5z8ZCtIKkT52XsJhw== +-----END CERTIFICATE----- diff --git a/rpc/lib/server/test.key b/rpc/lib/server/test.key new file mode 100644 index 000000000..bb9af06b0 --- /dev/null +++ b/rpc/lib/server/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEA0Fprp1z0ZlwiEAdapWvHQs41h/4iVu9M/vKtXjnCzSLHxNfU +1Q6INjVzDMofxrL8dhfjl7Z8Uuu0uJ1z2H5qmwLnmTdV1zLTT4qyv9p2NHwPgD8a +mdNL6FUMdOWog171rtRKiTfhmvWtzd+Hgia7lHES6uMzCQD/fqTr46lcynaqUphL +V8arCfYoaFeSn/JNZkgsQxcFrM3EbHjF+phfNabpuh7IkBekzYM2S/t23Br8JZhZ +pxRAkCk0ifCfn4Hwsuij6lqmmR9k6MAP39ocqzIVNJaaZZ6DtX/+i4He7m8i2p8Y +/7fGY+B5h4TI4h78/Z/xtG5Cz4YbBIKaLoXUZQIDAQABAoH/NodzpVmunRt/zrIe +By0t+U3+tJjOY/I9NHxO41o6oXV40wupqBkljQpwEejUaCxv5nhaGFqqLwmBQs/y +gbaUL/2Sn4bb8HZc13R1U8DZLuNJK0dYrumd9DBOEkoI0FkJ87ebyk3VvbiOxFK8 +JFP+w9rUGKVdtf2M4JhJJEwu/M2Yawx9/8CrCIY2G6ufaylrIysLeQMsxrogF8n4 +hq7fyqveWRzxhqUxS2fp9Ynpx4jnd1lMzv+z3i8eEsW+gB9yke7UkXZMbtZg1xfB +JjiEfcDVfSwSihhgOYttgQ9hkIdohDUak7OzRSWVBuoxWUhMfrQxw/HZlgZJL9Vf +rGdlAoGBANOGmgEGky+acV33WTWGV5OdAw6B/SlBEoORJbj6UzQiUz3hFH/Tgpbj +JOKHWGbGd8OtOYbt9JoofGlNgHA/4nAEYAc2HGa+q0fBwMUflU0DudAxXis4jDmE +D76moGmyJoSgwVrp1W/vwNixA5RpcZ3Wst2nf9RKLr+DxypHTit/AoGBAPwpDeqc +rwXOTl0KR/080Nc11Z03VIVZAGfA59J73HmADF9bBVlmReQdkwX0lERchdzD0lfa +XqbqBLr4FS5Uqyn5f3DCaMnOeKfvtGw2z6LnY+w03mii4PEW/vNKLlB18NdduPwL +KeAc08Zh+qJFMKD1PoEQOH+Y7NybBbaQL8IbAoGAfPPUYaq6o7I+Kd4FysKTVVW5 +CobrP8V65FGH0R++qttkBPfDHkeZqvx/O3nsVLoE4YigpP5IMhCcfbAUoTp7zuQm +vdvPJzqW/4qLD2c60QXUbBHdqPZ8jzVd/6d6tzVP36T+02+yb69XYiofDTrErRK5 +EorxzjwMJYH40xbQLI0CgYBh7d/FucwPSSwN3ixPIQtKSVIImLBuiT4rDTP6/reF +SEGF1ueg7KNAEGxE59OdKQGj1zkdfWU9Fa14n1g6gg9nYcoolJf1qAYb0nAThsXk +0lBwL6ggowERIIkrGygZf3Rlb7SjzgIZU5i7dtnLo2tbV2NK5G3MwCtdEaeKWzzw ++QKBgQC7+JPHoqbnNgis2vCGLKMOU3HpJK/rYEU/8ZUegc9lshEFZYsRbtKQQJQs +nqsChrG8UoK84frujEBkO/Nzsil85p8ar79wZguGnVvswTWaTuKvl8H/qQQ/JSHZ +OHGQD4qwTCkdRr8Vf8NfuCoZlJDnHncLJZNWjrb5feqCnJ/YIQ== +-----END RSA PRIVATE KEY----- From 3e7752c29d33c1c11bb2ba0ba9ac6985d47d83e1 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 22 Jun 2019 19:48:01 +0400 Subject: [PATCH 074/211] cs: exit if SwitchToConsensus fails (#3706) Refs #3656 --- CHANGELOG_PENDING.md | 1 + consensus/reactor.go | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 1bff64b64..5e9a222d1 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -32,6 +32,7 @@ ### FEATURES: ### IMPROVEMENTS: +- [consensus] \#3656 Exit if SwitchToConsensus fails - [p2p] \#3666 Add per channel telemetry to improve reactor observability - [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) - [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) diff --git a/consensus/reactor.go b/consensus/reactor.go index f690a407d..dc3514b21 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -116,8 +116,13 @@ func (conR *ConsensusReactor) SwitchToConsensus(state sm.State, blocksSynced int } err := conR.conS.Start() if err != nil { - conR.Logger.Error("Error starting conS", "err", err) - return + panic(fmt.Sprintf(`Failed to start consensus state: %v + +conS: +%+v + +conR: +%+v`, err, conR.conS, conR)) } } From a44c621d2dee57e083641df05b51ad4fd90f0c14 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 24 Jun 2019 15:48:21 +0200 Subject: [PATCH 075/211] Remove double lint (#3748) - Circel CI and Golangci were doing linting jobs, removed circleci Signed-off-by: Marko Baricevic --- .circleci/config.yml | 65 ++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 04a03eb25..5836b4546 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,22 +67,6 @@ jobs: export PATH="$GOBIN:$PATH" make build-slate - lint: - <<: *defaults - steps: - - attach_workspace: - at: /tmp/workspace - - restore_cache: - key: v4-pkg-cache - - restore_cache: - key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - - run: - name: metalinter - command: | - set -ex - export PATH="$GOBIN:$PATH" - make lint - test_abci_apps: <<: *defaults steps: @@ -98,8 +82,8 @@ jobs: export PATH="$GOBIN:$PATH" bash abci/tests/test_app/test.sh -# if this test fails, fix it and update the docs at: -# https://github.com/tendermint/tendermint/blob/develop/docs/abci-cli.md + # if this test fails, fix it and update the docs at: + # https://github.com/tendermint/tendermint/blob/develop/docs/abci-cli.md test_abci_cli: <<: *defaults steps: @@ -169,24 +153,24 @@ jobs: command: bash test/persist/test_failure_indices.sh localnet: - working_directory: /home/circleci/.go_workspace/src/github.com/tendermint/tendermint - machine: - image: circleci/classic:latest - environment: - GOBIN: /home/circleci/.go_workspace/bin - GOPATH: /home/circleci/.go_workspace/ - GOOS: linux - GOARCH: amd64 - parallelism: 1 - steps: - - checkout - - run: - name: run localnet and exit on failure - command: | - set -x - docker run --rm -v "$PWD":/go/src/github.com/tendermint/tendermint -w /go/src/github.com/tendermint/tendermint golang make build-linux - make localnet-start & - ./scripts/localnet-blocks-test.sh 40 5 10 localhost + working_directory: /home/circleci/.go_workspace/src/github.com/tendermint/tendermint + machine: + image: circleci/classic:latest + environment: + GOBIN: /home/circleci/.go_workspace/bin + GOPATH: /home/circleci/.go_workspace/ + GOOS: linux + GOARCH: amd64 + parallelism: 1 + steps: + - checkout + - run: + name: run localnet and exit on failure + command: | + set -x + docker run --rm -v "$PWD":/go/src/github.com/tendermint/tendermint -w /go/src/github.com/tendermint/tendermint golang make build-linux + make localnet-start & + ./scripts/localnet-blocks-test.sh 40 5 10 localhost test_p2p: environment: @@ -273,9 +257,9 @@ jobs: paths: - "release-version.source" - save_cache: - key: v2-release-deps-{{ checksum "go.sum" }} - paths: - - "/go/pkg/mod" + key: v2-release-deps-{{ checksum "go.sum" }} + paths: + - "/go/pkg/mod" build_artifacts: <<: *defaults @@ -358,9 +342,6 @@ workflows: - master - develop - setup_dependencies - - lint: - requires: - - setup_dependencies - test_abci_apps: requires: - setup_dependencies From c37faf3ac18b02b961430d0b1159197912ea7d2b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 24 Jun 2019 17:49:49 +0400 Subject: [PATCH 076/211] docs: (rpc/broadcast_tx_*) write expectations for a client (#3749) Refs #3322 --- rpc/core/mempool.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/rpc/core/mempool.go b/rpc/core/mempool.go index 967466e73..1a0954438 100644 --- a/rpc/core/mempool.go +++ b/rpc/core/mempool.go @@ -19,6 +19,19 @@ import ( // Returns right away, with no response. Does not wait for CheckTx nor // DeliverTx results. // +// If you want to be sure that the transaction is included in a block, you can +// subscribe for the result using JSONRPC via a websocket. See +// https://tendermint.com/docs/app-dev/subscribing-to-events-via-websocket.html +// If you haven't received anything after a couple of blocks, resend it. If the +// same happens again, send it to some other node. A few reasons why it could +// happen: +// +// 1. malicious node can drop or pretend it had committed your tx +// 2. malicious proposer (not necessary the one you're communicating with) can +// drop transactions, which might become valid in the future +// (https://github.com/tendermint/tendermint/issues/3322) +// 3. node can be offline +// // Please refer to // https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting // for formatting/encoding rules. @@ -69,6 +82,18 @@ func BroadcastTxAsync(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadca // Returns with the response from CheckTx. Does not wait for DeliverTx result. // +// If you want to be sure that the transaction is included in a block, you can +// subscribe for the result using JSONRPC via a websocket. See +// https://tendermint.com/docs/app-dev/subscribing-to-events-via-websocket.html +// If you haven't received anything after a couple of blocks, resend it. If the +// same happens again, send it to some other node. A few reasons why it could +// happen: +// +// 1. malicious node can drop or pretend it had committed your tx +// 2. malicious proposer (not necessary the one you're communicating with) can +// drop transactions, which might become valid in the future +// (https://github.com/tendermint/tendermint/issues/3322) +// // Please refer to // https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting // for formatting/encoding rules. From d88a639838d03dcc76c0b1f04b589267228db5cd Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 24 Jun 2019 10:32:12 -0400 Subject: [PATCH 077/211] Make RPC bind to localhost by default (#3746) * Make RPC bind to localhost by default * Add CHANGELOG_PENDING entry * Allow testnet command to override RPC listen address * Update localnet test to bind RPC to 0.0.0.0 * Update p2p test to bind RPC to 0.0.0.0 * Remove rpc-laddr parameter * Update localnet to use config template with RPC listen address override * Use config template override method for RPC listen address * Build config template into localnode image * Build localnode image locally before starting localnet * Move testnet config overrides into templates * Revert deletion of config overrides * Remove extraneous config parameter overrides --- CHANGELOG_PENDING.md | 1 + Makefile | 4 ++-- config/config.go | 2 +- networks/local/localnode/Dockerfile | 2 +- networks/local/localnode/config-template.toml | 2 ++ test/docker/Dockerfile | 7 ++++++- test/docker/config-template.toml | 2 ++ 7 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 networks/local/localnode/config-template.toml create mode 100644 test/docker/config-template.toml diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 5e9a222d1..26fb14d1f 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -36,6 +36,7 @@ - [p2p] \#3666 Add per channel telemetry to improve reactor observability - [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) - [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) +- [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` ### BUG FIXES: - [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) diff --git a/Makefile b/Makefile index 3dff35b9c..35baf7d3a 100644 --- a/Makefile +++ b/Makefile @@ -275,8 +275,8 @@ build-docker-localnode: @cd networks/local && make # Run a 4-node testnet locally -localnet-start: localnet-stop - @if ! [ -f build/node0/config/genesis.json ]; then docker run --rm -v $(CURDIR)/build:/tendermint:Z tendermint/localnode testnet --v 4 --o . --populate-persistent-peers --starting-ip-address 192.167.10.2 ; fi +localnet-start: localnet-stop build-docker-localnode + @if ! [ -f build/node0/config/genesis.json ]; then docker run --rm -v $(CURDIR)/build:/tendermint:Z tendermint/localnode testnet --config /etc/tendermint/config-template.toml --v 4 --o . --populate-persistent-peers --starting-ip-address 192.167.10.2; fi docker-compose up # Stop testnet diff --git a/config/config.go b/config/config.go index 6c4654fed..aa25cff6b 100644 --- a/config/config.go +++ b/config/config.go @@ -369,7 +369,7 @@ type RPCConfig struct { // DefaultRPCConfig returns a default configuration for the RPC server func DefaultRPCConfig() *RPCConfig { return &RPCConfig{ - ListenAddress: "tcp://0.0.0.0:26657", + ListenAddress: "tcp://127.0.0.1:26657", CORSAllowedOrigins: []string{}, CORSAllowedMethods: []string{"HEAD", "GET", "POST"}, CORSAllowedHeaders: []string{"Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"}, diff --git a/networks/local/localnode/Dockerfile b/networks/local/localnode/Dockerfile index 3942cecd6..03af5aa3c 100644 --- a/networks/local/localnode/Dockerfile +++ b/networks/local/localnode/Dockerfile @@ -13,4 +13,4 @@ CMD ["node", "--proxy_app", "kvstore"] STOPSIGNAL SIGTERM COPY wrapper.sh /usr/bin/wrapper.sh - +COPY config-template.toml /etc/tendermint/config-template.toml diff --git a/networks/local/localnode/config-template.toml b/networks/local/localnode/config-template.toml new file mode 100644 index 000000000..a90eb7bd5 --- /dev/null +++ b/networks/local/localnode/config-template.toml @@ -0,0 +1,2 @@ +[rpc] +laddr = "tcp://0.0.0.0:26657" diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 77cc515e5..b39277bd9 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -27,7 +27,12 @@ RUN make install_abci # install Tendermint RUN make install -RUN tendermint testnet --node-dir-prefix="mach" --v=4 --populate-persistent-peers=false --o=$REPO/test/p2p/data +RUN tendermint testnet \ + --config $REPO/test/docker/config-template.toml \ + --node-dir-prefix="mach" \ + --v=4 \ + --populate-persistent-peers=false \ + --o=$REPO/test/p2p/data # Now copy in the code # NOTE: this will overwrite whatever is in vendor/ diff --git a/test/docker/config-template.toml b/test/docker/config-template.toml new file mode 100644 index 000000000..a90eb7bd5 --- /dev/null +++ b/test/docker/config-template.toml @@ -0,0 +1,2 @@ +[rpc] +laddr = "tcp://0.0.0.0:26657" From 4c4bf0f1316e0e9e1dfdda516cd1179a30d5adfb Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 24 Jun 2019 16:32:24 +0200 Subject: [PATCH 078/211] Added flags '-s -w' to buildflags (#3742) * Added flags '-s -w' to buildflags Signed-off-by: Marko Baricevic * added a condition for no strip * minor changes * on call discussions * on call discussion v2 --- Makefile | 3 ++- go.mod | 45 ++++++++++++++++++++++----------------------- go.sum | 24 ++++++++++++++++++++---- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 35baf7d3a..f16e62560 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,8 @@ export GO111MODULE = on INCLUDE = -I=. -I=${GOPATH}/src -I=${GOPATH}/src/github.com/gogo/protobuf/protobuf BUILD_TAGS?='tendermint' -BUILD_FLAGS = -mod=readonly -ldflags "-X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD`" +LD_FLAGS = -X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD` -s -w +BUILD_FLAGS = -mod=readonly -ldflags "$(LD_FLAGS)" all: check build test install diff --git a/go.mod b/go.mod index 8fe1a124b..f4a4c6dda 100644 --- a/go.mod +++ b/go.mod @@ -3,50 +3,49 @@ module github.com/tendermint/tendermint go 1.12 require ( - github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/VividCortex/gohistogram v1.0.0 // indirect + github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a - github.com/davecgh/go-spew v1.1.1 github.com/etcd-io/bbolt v1.3.2 github.com/fortytw2/leaktest v1.2.0 - github.com/fsnotify/fsnotify v1.4.7 github.com/go-kit/kit v0.6.0 github.com/go-logfmt/logfmt v0.3.0 - github.com/go-stack/stack v1.8.0 + github.com/go-stack/stack v1.8.0 // indirect github.com/gogo/protobuf v1.2.1 + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/protobuf v1.3.0 - github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/google/gofuzz v1.0.0 // indirect github.com/gorilla/websocket v1.2.0 - github.com/hashicorp/hcl v1.0.0 - github.com/inconshreveable/mousetrap v1.0.0 + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmhodges/levigo v1.0.0 - github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 + github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect github.com/magiconair/properties v1.8.0 - github.com/matttproud/golang_protobuf_extensions v1.0.1 - github.com/mitchellh/mapstructure v1.1.2 - github.com/pelletier/go-toml v1.2.0 + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect github.com/pkg/errors v0.8.0 - github.com/pmezard/go-difflib v1.0.0 github.com/prometheus/client_golang v0.9.1 - github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 - github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 - github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d + github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect + github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 // indirect + github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d // indirect github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 github.com/rs/cors v1.6.0 - github.com/spf13/afero v1.1.2 - github.com/spf13/cast v1.3.0 + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.0 // indirect github.com/spf13/cobra v0.0.1 - github.com/spf13/jwalterweatherman v1.0.0 - github.com/spf13/pflag v1.0.3 + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.3 // indirect github.com/spf13/viper v1.0.0 github.com/stretchr/testify v1.2.2 github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e github.com/tendermint/go-amino v0.14.1 + go.etcd.io/bbolt v1.3.3 // indirect golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a - golang.org/x/text v0.3.0 - google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 + google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 // indirect google.golang.org/grpc v1.13.0 - gopkg.in/yaml.v2 v2.2.1 ) diff --git a/go.sum b/go.sum index fee691de9..2a349d300 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,13 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= @@ -29,23 +32,31 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -54,7 +65,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -86,22 +99,22 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.0.0 h1:RUA/ghS2i64rlnn4ydTfblY8Og8QzcPtCcHvgMn+w/I= github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e h1:91EeXI4y4ShkyzkMqZ7QP/ZTIqwXp3RuDu5WFzxcFAs= github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20180710023853-292b43bbf7cb/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= @@ -112,8 +125,11 @@ google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 h1:67iHsV9djwGdZpd google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.13.0 h1:bHIbVsCwmvbArgCJmLdgOdHFXlKqTOVjbibbS19cXHc= google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From e93e8e730d8d3044db7d7ae78d20af036332fafa Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 24 Jun 2019 20:13:25 +0200 Subject: [PATCH 079/211] Added a disclaimer so the user is aware of binary (#3751) * Added a disaclaimer so the user is aware of binary - added disclaimer for `-s -w` Signed-off-by: Marko Baricevic * move up to compile --- docs/introduction/install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/introduction/install.md b/docs/introduction/install.md index 4f35ffef7..00e04fa0b 100644 --- a/docs/introduction/install.md +++ b/docs/introduction/install.md @@ -45,6 +45,8 @@ make build to put the binary in `./build`. +_DISCLAIMER_ The binary of tendermint is build/installed without the DWARF symbol table. If you would like to build/install tendermint with the DWARF symbol and debug information, remove `-s -w` from `BUILD_FLAGS` in the make file. + The latest `tendermint version` is now installed. ## Run From eb08609de1b6a057af1d9e42591bb5215f5fb169 Mon Sep 17 00:00:00 2001 From: Mengjay Gao <1955889005@qq.com> Date: Tue, 25 Jun 2019 14:56:03 +0800 Subject: [PATCH 080/211] docs: update JS section of abci-cli.md (#3747) --- docs/app-dev/abci-cli.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/app-dev/abci-cli.md b/docs/app-dev/abci-cli.md index b09b9a11b..7e9db91b9 100644 --- a/docs/app-dev/abci-cli.md +++ b/docs/app-dev/abci-cli.md @@ -326,10 +326,18 @@ application easily in any language. We have implemented the counter in a number of languages [see the example directory](https://github.com/tendermint/tendermint/tree/develop/abci/example). -To run the Node JS version, `cd` to `example/js` and run +To run the Node.js version, fist download & install [the Javascript ABCI server](https://github.com/tendermint/js-abci): ``` -node app.js +git clone https://github.com/tendermint/js-abci.git +cd js-abci +npm install abci +``` + +Now you can start the app: + +```bash +node example/counter.js ``` (you'll have to kill the other counter application process). In another From 747f99fdc198d7ae6456b010c9b8857aae97e25f Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 25 Jun 2019 07:57:50 -0400 Subject: [PATCH 081/211] changelog and version (#3750) * changelog and version * Add section on ABCI changes * Update ABCI upgrade section to include events examples * update upgrading * more upgrading and changelog * update changelog from pending * refer to #rpc_changes * minor word changes --- CHANGELOG.md | 63 ++++++++++++++++++++++++++ CHANGELOG_PENDING.md | 31 +++---------- UPGRADING.md | 102 ++++++++++++++++++++++++++++++++++++++++++- version/version.go | 2 +- 4 files changed, 171 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 068d99c3c..76f547a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## v0.32.0 + +*June 25, 2019* + +Special thanks to external contributors on this release: +@needkane, @SebastianElvis, @andynog, @Yawning, @wooparadog + +This release contains breaking changes to our build and release processes, ABCI, +and the RPC, namely: +- Use Go modules instead of dep +- Bring active development to the `master` Github branch +- ABCI Tags are now Events - see + [docs](https://github.com/tendermint/tendermint/blob/60827f75623b92eff132dc0eff5b49d2025c591e/docs/spec/abci/abci.md#events) +- Bind RPC to localhost by default, not to the public interface [UPGRADING/RPC_Changes](./UPGRADING.md#rpc_changes) + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### BREAKING CHANGES: + +* CLI/RPC/Config + - [cli] \#3613 Switch from golang/dep to Go Modules to resolve dependencies: + It is recommended to switch to Go Modules if your project has tendermint as + a dependency. Read more on Modules here: + https://github.com/golang/go/wiki/Modules + - [config] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed `leveldb` as generic + option for `db_backend`. Must be `goleveldb` or `cleveldb`. + - [rpc] \#3616 Fix field names for `/block_results` response (eg. `results.DeliverTx` + -> `results.deliver_tx`). See docs for details. + - [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` + +* Apps + - [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, + and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` + contains a `type` and a list of `attributes` (list of key-value pairs) + allowing for inclusion of multiple distinct events in each response. + +* Go API + - [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI + Application interface + - [libs/db] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed deprecated `LevelDBBackend` const + If you have `db_backend` set to `leveldb` in your config file, please + change it to `goleveldb` or `cleveldb`. + - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID + +* Blockchain Protocol + +* P2P Protocol + +### FEATURES: + +### IMPROVEMENTS: +- [abci/examples] \#3659 Change validator update tx format in the `persistent_kvstore` to use base64 for pubkeys instead of hex (@needkane) +- [consensus] \#3656 Exit if SwitchToConsensus fails +- [p2p] \#3666 Add per channel telemetry to improve reactor observability +- [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) + +### BUG FIXES: +- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) +- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [node] \#3716 Fix a bug where `nil` is recorded as node's address +- [node] \#3741 Fix profiler blocking the entire node + ## v0.31.7 *June 3, 2019* diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 26fb14d1f..45d1c7899 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,29 +1,19 @@ -## v0.31.8 +## v0.32.1 ** +Special thanks to external contributors on this release: + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + ### BREAKING CHANGES: * CLI/RPC/Config - - [cli] \#3613 Switch from golang/dep to Go Modules to resolve dependencies: - It is recommended to switch to Go Modules if your project has tendermint as - a dependency. Read more on Modules here: - https://github.com/golang/go/wiki/Modules - - [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` - -> `results.deliver_tx`). See docs for details. * Apps - - [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, - and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` - contains a `type` and a list of `attributes` (list of key-value pairs) - allowing for inclusion of multiple distinct events in each response. * Go API - - [libs/db] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed deprecated `LevelDBBackend` const - If you have `db_backend` set to `leveldb` in your config file, please - change it to `goleveldb` or `cleveldb`. - - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID - - [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI interface * Blockchain Protocol @@ -32,14 +22,5 @@ ### FEATURES: ### IMPROVEMENTS: -- [consensus] \#3656 Exit if SwitchToConsensus fails -- [p2p] \#3666 Add per channel telemetry to improve reactor observability -- [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) -- [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) -- [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` ### BUG FIXES: -- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) -- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) -- [node] \#3716 Fix a bug where `nil` is recorded as node's address -- [node] \#3741 Fix profiler blocking the entire node diff --git a/UPGRADING.md b/UPGRADING.md index 5a77e0729..af42d2a66 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,14 +3,114 @@ This guide provides steps to be followed when you upgrade your applications to a newer version of Tendermint Core. - ## v0.32.0 +This release is compatible with previous blockchains, +however the new ABCI Events mechanism may create some complexity +for nodes wishing to continue operation with v0.32 from a previous version. +There are some minor breaking changes to the RPC. + ### Config Changes If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. +### RPC Changes + +The default listen address for the RPC is now `127.0.0.1`. If you want to expose +it publicly, you have to explicitly configure it. Note exposing the RPC to the +public internet may not be safe - endpoints which return a lot of data may +enable resource exhaustion attacks on your node, causing the process to crash. + +Any consumers of `/block_results` need to be mindful of the change in all field +names from CamelCase to Snake case, eg. `results.DeliverTx` is now `results.deliver_tx`. +This is a fix, but it's breaking. + +### ABCI Changes + +ABCI responses which previously had a `Tags` field now have an `Events` field +instead. The original `Tags` field was simply a list of key-value pairs, where +each key effectively represented some attribute of an event occuring in the +blockchain, like `sender`, `receiver`, or `amount`. However, it was difficult to +represent the occurence of multiple events (for instance, multiple transfers) in a single list. +The new `Events` field contains a list of `Event`, where each `Event` is itself a list +of key-value pairs, allowing for more natural expression of multiple events in +eg. a single DeliverTx or EndBlock. Note each `Event` also includes a `Type`, which is meant to categorize the +event. + +For transaction indexing, the index key is +prefixed with the event type: `{eventType}.{attributeKey}`. +If the same event type and attribute key appear multiple times, the values are +appended in a list. + +To make queries, include the event type as a prefix. For instance if you +previously queried for `recipient = 'XYZ'`, and after the upgrade you name your event `transfer`, +the new query would be for `transfer.recipient = 'XYZ'`. + +Note that transactions indexed on a node before upgrading to v0.32 will still be indexed +using the old scheme. For instance, if a node upgraded at height 100, +transactions before 100 would be queried with `recipient = 'XYZ'` and +transactions after 100 would be queried with `transfer.recipient = 'XYZ'`. +While this presents additional complexity to clients, it avoids the need to +reindex. Of course, you can reset the node and sync from scratch to re-index +entirely using the new scheme. + +We illustrate further with a more complete example. + +Prior to the update, suppose your `ResponseDeliverTx` look like: + +```go +abci.ResponseDeliverTx{ + Tags: []cmn.KVPair{ + {Key: []byte("sender"), Value: []byte("foo")}, + {Key: []byte("recipient"), Value: []byte("bar")}, + {Key: []byte("amount"), Value: []byte("35")}, + } +} +``` + +The following queries would match this transaction: + +```go +query.MustParse("tm.event = 'Tx' AND sender = 'foo'") +query.MustParse("tm.event = 'Tx' AND recipient = 'bar'") +query.MustParse("tm.event = 'Tx' AND sender = 'foo' AND recipient = 'bar'") +``` + +Following the upgrade, your `ResponseDeliverTx` would look something like: +the following `Events`: + +```go +abci.ResponseDeliverTx{ + Events: []abci.Event{ + { + Type: "transfer", + Attributes: cmn.KVPairs{ + {Key: []byte("sender"), Value: []byte("foo")}, + {Key: []byte("recipient"), Value: []byte("bar")}, + {Key: []byte("amount"), Value: []byte("35")}, + }, + } +} +``` + +Now the following queries would match this transaction: + +```go +query.MustParse("tm.event = 'Tx' AND transfer.sender = 'foo'") +query.MustParse("tm.event = 'Tx' AND transfer.recipient = 'bar'") +query.MustParse("tm.event = 'Tx' AND transfer.sender = 'foo' AND transfer.recipient = 'bar'") +``` + +For further documentation on `Events`, see the [docs](https://github.com/tendermint/tendermint/blob/60827f75623b92eff132dc0eff5b49d2025c591e/docs/spec/abci/abci.md#events). + +### Go Applications + +The ABCI Application interface changed slightly so the CheckTx and DeliverTx +methods now take Request structs. The contents of these structs are just the raw +tx bytes, which were previously passed in as the argument. + + ## v0.31.6 There are no breaking changes in this release except Go API of p2p and diff --git a/version/version.go b/version/version.go index 1a15717f7..2d1c41fd5 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.31.7" + TMCoreSemVer = "0.32.0" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.0" From 1014f8c84bb10300f43f246642d69f46bd8954fd Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 28 Jun 2019 17:32:15 +0200 Subject: [PATCH 082/211] docs: update to contributing.md (#3760) * Update to contributing.md - Add in section for Draft PR, instead of WIP - Add make lint as part of the commit process. Signed-off-by: Marko Baricevic * mistake * minor word change --- CONTRIBUTING.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 77a625504..ef992fee6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,9 @@ to write a more detailed design document in the form of an Architectural Decision Record (ie. see [here](./docs/architecture/)) before submitting code changes. -Please make sure to use `gofmt` before every commit - the easiest way to do this is have your editor run it for you upon saving a file. +Please open a [Draft PR](https://github.blog/2019-02-14-introducing-draft-pull-requests/), even if your contribution is incomplete, this inidicates to the community you're working on something and allows them to provide comments early in the development process. When the code is complete it can be marked as ready-for-review. + +Please make sure to use `gofmt` before every commit - the easiest way to do this is have your editor run it for you upon saving a file. Additionally please ensure that your code is lint compliant by running `make lint` ## Forking From b2afc6590858c0804bdca478a76ea9d36f0181c6 Mon Sep 17 00:00:00 2001 From: Peng Zhong <172531+nylira@users.noreply.github.com> Date: Sat, 29 Jun 2019 03:06:44 -0400 Subject: [PATCH 083/211] docs: add readme image (#3763) --- README.md | 1 + docs/tendermint-core-image.jpg | Bin 0 -> 125536 bytes 2 files changed, 1 insertion(+) create mode 100755 docs/tendermint-core-image.jpg diff --git a/README.md b/README.md index ec7b28cc0..994ca63b2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Tendermint +![banner](docs/tendermint-core-image.jpg) [Byzantine-Fault Tolerant](https://en.wikipedia.org/wiki/Byzantine_fault_tolerance) [State Machines](https://en.wikipedia.org/wiki/State_machine_replication). diff --git a/docs/tendermint-core-image.jpg b/docs/tendermint-core-image.jpg new file mode 100755 index 0000000000000000000000000000000000000000..75832e602dbb36010e1812de3e5dd0c5969a47f9 GIT binary patch literal 125536 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%0Dx$6!G)u1{;V=V=17T`y9g)& z?G_%~L$P0iSCH(Lc9A;Fdc*CB{9IB%v#2PzD#~;p;E!1Qh#zb0z%q}B8L#n~ya%x% ztpnMQ?koI9iBy=4+#I_sAOJ6^ZEplU&R;mp151L};WYyX{jAQA*aJ1> z(+{kwz0uBgh#~o}z=Y{z{{XbAbQEz+q?m4K(tOGu!UvAX!Dg6C%#DB{ZBf3VO0zJ( z1hftu*Th`cb#lFQ;#~6}cO=>E2uj29m-tLIJ&=wl0bZGgnCXXh$hUpq2$+CjAO$&2 zj^zs!u)I<*Qeq$hj;Ty<(%>LWP-Vcl3%FFw0tlL0h(o!Qj~1dA5Z&gc+m0FRBN>{( zE6Xd2iV=lBrL>lCaD6K$oI{n4{{V0&fr~yeg-=8GJ{09$vp|cunfx_MyCCZv*t7?*eT%{Q{qBX zuo;Ma%fFd?IK`F_<{n`{_dF?{#?=z%yUEefwJN4L>moz*%}!9A}pX{Qv&={z^4bY`x64as-Fu;@Q$LXjV9aa zhZpIofP*ww8T_r_-q&mz#p0$^@%?a-B#4?w-?_gmK5|05j+hBgbuTJhIQW(EmI{x9 zjr`ET(&dbA6%vYM+}$XrbkE1JZp068teq^AC%n z-o|^cHlH+O7-}($PNuBY75#_|t7t$J2))>1873u_rV_0X>NRW-TV;%=K-nz%wPP*m z(~8$@ic_squZ@HeD{SG0NTM2F%n5iVAKK$jc|skQEWa>eWYL*mdot_xN`WrmwnCxe zGLQzSfL6T+mAzJcOmEJ>od8;w2P(!>UtysM%7W zd?3J%;TMpIEQA`w6JixMrdvzo)-Q^d5p7Sd!n` zUp;9YVL@`OWYM=rq6kcb$WzWqBbVRR>d*X z^6<|CD{`o&(UgrL?Hp`T&WkrHd$kN$eWr!KFlHCpDcrYgw~O?NFMvCWY!P0o;spqC z(D-|M65hvSh4^_A&#{2_Sl|OxG+%pjbW(eM4+LRt-$eLffU(K5{!F(f>$ZL6g=5ai zNx5;mLQ{=jbjTwa#6E7}xkU)nQOd7$y^uBqA>j_1O~I-a!EmS>fRG#kW`0O0M(I-HYUf#{_H;?0im==R`+&R+uT9ebEG`F;~emhOER2 ztt0Izx-1f!{o%4>_KGbh#0_QqK&qR)N^HEWxZnvb1{?6qfl|aOT-7m}racjzP>N=l zj5-LV+%?;AvjGO&Nw{9fJ#fnQ{;r_0eFrB5A;J#;hG*U@e_%gCR1%xnhsIHg^b_Cb z9ac42j}HvfFEQH%+r-JdTL{d8gky|jN}nd!{0St9`-87$*U#cU!KZ>DQLVIZ%)j|s_zvn63s z550zFT&Vv5)lA~Ul%-5xlo_R$!i^$IX&57jI;3X?Bi15DlHRf5n^QAR<uwo0W31m9>!Rp`z%Acz>KEWVi9o`X5-5Rdq;rK z)A6)@_|6Et80uMn0>YJ8$3cN&2&-t545cvssH(|Z)j&9iGQqXzM0$Ras1gSPQ zG$Mmhq9VDUE9`0wKM?@O^Azvt);T=`!3q%Q11kq}Xo5eAdQJh#^0vL+Q6*#iJJIgO zTU=^!7)O)y6&pxIiSN`wSUG>F_bE|Xvk7PdETa&v-T>5-OW%_c+fMca$r6~#Z-MO_ z28t`BVD?D=0EPf`bb4+mqz37XK;c^o&9SEb95808t@Ryb2BFIPiq2Mf#B2c+5aVg0 zI=?QXP3+kU4QgPd%4qlup|6vNfkmhkyfOKfAZ{sO60;iKCJ5C*wCrU{FSa7^INOvy zH6M68)S8mG3Y`KWI;-kWbA}#4+4Mmg){pJ12{yhdUixB=19Ide$+sp%dj&~c> z+Q9oHco>6I;iwFsq;>3_F&b{}Fnj(PaaES+9Qm1L#_=xXVsQ)a!lXTa*vtZ-6690F zOFBk3S7XK%wY7D5#WJ(ML~)wu`od&vcq2lM9?@5C)vr~)=4x; z5Tr#rWhM)18;X_ z>N7#gjxE#s#P|pdO+=k=YB#o60L!zlXb7WO zAsy(w7rKS8)$5{KI{;t2w^|gw@{=A9j7JbFqx*_aPMB3AnDnlgk&FznAjJ=oQh(=g zvLUay_aC*pvMgoU3l8kYq%W?<6)lq4Z<%ml+gKnfjTqz5gr`UA>yjLvk`pwT9|Uj@ z*^UpQOQ5Tt(Ge|{eq74Ez^DOy6KciE$ACDu_d!K5{{Rl$Ibj&^phg4S98!{!VroJ- zi^s*V@8s?kgB^{cG$$FXktpFgml1g~)W&O>g2dKDoH3R(U2F5BWb(L`03=(%w89;@Q^0-u?tqW~Hl#gbTX4XWss_u3kN zetK$!;P})%iDbEQ;LDdT74A_Bvq=Iuc|lhBPRK;9-?Q7yw^m}WfyOIaJrKvY;w7N_ zpgDrs*U1nXABa)Jv2;vE>Am>lBa#PWrtt(L6dwtga4Q|Q95owORO)8~2q<@UtwJ$c%@D1 zJl#Q`po2{ZkZGvN{wP!Q_Hk}8!BfQSKKokhI0F>R(jYhrX} zfM%b?%6Z5MiDjd}^j{3z45E^v9nMjTwU-EPE*2a4BTMAXS5V$E`U;5dZ3TQ#)?xA@ z21Io*WOht^(9K3X_HBE*VKGtV!=VdYghG)b{4QOn@EhhbhPQ@Tq`6c#x04k5 zSJPY`wa`EotI!h5?I73vM}!|`{I*nFyrouHVhb=0fSRl7A4u_Oz=Z%&G{;24gml7E zQ?k?V8jdzQAtx`x2F1vR7Ab*Y1496}2pZqyDw;J`v}uMxjx z0!9od1+M_+46vgMEFn>HU+iI+qxhDHzv7r8%oa$%4X~wH!|c-aBcZ!ME6L)j>xbVigsUUnxLSY^k1Ol^<21xnM!C6JEt9Tv!9NPu^CCQD2RL>SSs3_jBFI< zA@aVfm&MDME)N&Q%Y_GZvT*h!A1#6a;;va|jQT90XW^K^z}(5*b=pP()#V(xPOE z3=-&+96>R?n){JNSLD>HC!r_K2Gs_HKz5wH{9MOj1+Pa#CAfoVMW>iUW!03CB z#uOgM=TIOTg%XPDcmFX(Ke`{Sa{0Inm;}eaFiGK@p55gD0)WvN z^Z-);4%i01W&Gx5rpon$(ctF!zfH^Kgj~6D;RFn|q2GyTkiKu2)Rb-2Oi{vqUJ$sT zBTKXaAPx9ppdawY?LHt%M0jec8;#+XSu6uqZf(~!Q&43R_JF0TiaL%1<^TnGept0- zRxXj1HtscER82lrP6-S)!7fZjKrN4PhjH{~+`qHreFxAvJ5131HGY)zUlvJc?XrLY zmejR5LRjOuqYwkD2b_6=vG_$7(8hET!(bqJWlxNNgI7nv%flGNmyEt8qWsTt7to@_ z-cX-ugGSiC3sE;1k<~ygVPR|#)BgadbIGOlyM!t92zz~G&-WYd#k9W2J1&UsX^lIw zf8qZCs7eP(2dbgzzAiYqrV<&Mc7IleqNw8g6TuhXAq@=7IG5k4v*6)V@9)tWd|#~f z48AU07uKd->S=LNDu3mi8TY5_md)+c92#yZhZ1U4HOf;~Awa_7Wjba6GD?E(pcq4-8{F zn;#P_$~+HAa+ddGH+SbTTJpR%=b$EO?Yj8PmRl@PcelGOq_il-CXtViQqSRXlrz9P z)9MtVHy~Lz2tJ1OAt z5t~elH=c6H-yTS_?FXS_7)^idNknYN&OLfbz-Qy+d!V$27b~#kNS-MP=u5b zz@(P!o42V1RL;7;B?b7@`~xn)*50e+ulYM@zMpR&{=&@0inrpONcLm-@8~XhRbu}D z(<4?}GM+}HM_s@c@5I(4tM3?xdmrUI-kk|0`Y+Rbo`>rUM<+7|j~9N-9+c2$a_TEJ zj>tC8<{{yZgf8w5kd6|bkgaLagtQuVMzap0+%3FITf<bc%NkH-uV#})bktPX`n8yq`BWn3tIGW4mBOV_UU_!MNtaWJj z>J@R>9+C}+Rp@1>Fa#&0P>SEg?lM;|4*d*&bkR1wrg5}CLzplnKcTpDev(}HU$H0- z@L?2h6ueoJWJH(Z1{Yn*O0o6=6)-?UxGIdeV1k%hGCLWP_X1@v*^OIf68OF@EEp~p z3^jYgFAssnsJp2~o`L0y8Dt$i5fe3kGQWREh_%hL&lh`m&HK$W-KrnWHFd8rFo5LF zTd#xK{{X1Au_$Zr@BScxJ*n)Sj4c`!2idL?16vv48D2Ut&tQ+Gy!;D*)~G@GcCwrq z_GX^pbE$PccSqfcP{y}k&-CXv^jJQNmj(4)%)bmW88AQGDv)RxVjS#xCc3$<4%LtT4n@8(d{o*j=5+ zXIE1z_JdhBsMvu5D&yjqKOl1O>(c(}(R)tl@AQPUimW+L((8nY(%wh{Fe&$AWo=8* zxUS$B9wMC>zMv2cL4<5?+I1r0SOXP;%`>B?+FUpyCdj-m1XWa{fGTai&6@!wOziQdbQ2^ z4^8@Si{kmeLu~O$Yz(ED=>azqntZFw9~1%y_J|{&CjyZILaAsn_%N}< zbV9)Zg?9e{+GE7qG<~8g<2UHXTxZee5JqdzZ?nK~J(3a9h0FeFAYX5Z>`2hxdV}3j z({*3(w6lVs7nbYgm6@R&Kblb6?z%h;4`cK<#riMl+_`6@OHwgjQ?Hcj<@gfK+(y&p zQc^d(?0^q&7)L61Y8W`)@|$b-F|R{p1pt;9i|7k$Qz>9&v4UkirD_>OQovbdrXm<1 z+Y38mt7L70)vpscE{+(XvoKJO+KPj6)fw&q9{pIYcvCxxgI|$>y&NG}B1!-QLHxe- zzp*FOwMCj~!1|8|sE?^QudHx@HnOw2S%u9nu@**FC}28yj+bT)Pj?`~%2g5fB`yoR zSE-9i5F(Gc#46A{+^}wd)Tcl;KT9u0|cX6}*6t#hL7z*r6lY&%-7Z-(?;1Zgq zo7{_qHHeF<<&Ao8h4f!6L51q!B-+=$$EVzfi=vRT5dfm)(z?Vq_|$%?{)PP`V@{In z%6-u<%Lve8l3pUNFwcsb%w!Y%9}up#rWm6SFNQj~dVG73O`i!v4pBekm6moUHWkS( zz_^TC0i0tqJ{eee?bCQ!~f$>C5^|dSB9yp|{)r0K@`0KoGRF&E-okaS=%sTWDb!{&8kIo7cZ=dSTAM~xO6JZU8FA*nYr(1f>A&iebK}->dTLa7SJXHeHCp)Sg}b5= zer*Al0_Dit1wiYIi|B0e4U!U5=AyAXSi>EyeTk?-kOm4dF?VN%YslqW#B5sZHcs}F z_Ubi*46J5brH5$19kfTvzSE|;X}7TOb}SE!8}G=)D%EQ!MK3&c!wvD*J|~|;zg66` zeBTGtuhQSFX%llMV50f+EiT!4h0Inaw=XNRa^RWp>RJnVz~;Low9X-&Sky;|VWuG%Lc64?q*Eb=o-M+q zLNS<_T4NQW6s0HZ1b&zECjJgjSq;Hd_B|({+j2r5A?8Q4eKntW}^Da9f+2S(d z4t#cW%^Q3&lOhq{mM|CIrNzKF9%k>nNKy;l;kg*sKgN98ub}pEY1)88B#jj zAY9Z$s^aL2FEO}a!pk+V;EG_`<;TQk43P#EP_{HkH*Z7$8p@DoU{q_lU|D~Nw(-}~ zS%0)&(Z5ac{&vXj`OGSPqog$e=VqY79?yCqI6kt4?O~qiWMlPsij*QPh-+hPp^DC# zb@amBFFxe76Gs z%WDn`4aLDgwBQL1JW4&`~4di{2G7($F;$s?M#32wZdx*4+ z_Ir>(Av)3)V=?I2(*?-Dt-`BD3@!lGlcb`EZGm#;3Y$vLyuV^S)8{*b5#m;O)YNv0 zD+UE>;yk~%?VGqho{9Y4PeC0p`o(%H&#lahJu*m&MFHUl-^eX`6>rq7MCtVyCIgZwersF`vL=3}FikxXC6Adquuq z*=t@Ht&gYID_4WmdQ~?WR@eubZmJ8!QRvZ!)UHbPC(9SHEIj~w5YJ<{6FsmGorlD$ zDboa$hA-rZFpPRh7)&WQ5KvKc5zlRa{iU`e8`y$J4kZk*+`vkK-b^@S6rrMk_1YU9 zv>snGI@`Fsz9w#_*}qQxJ#l`V{;lHW`Y(}}E?)=GXBitK6Q%nZd8#=f*%bn7gw6Xt zCBGT#xcMxDFFncbTl}FCFh+pSa2Su0I*InK9wuP9i-~JpN(PEaiFGnL&+LlZ83;P; z%*S&QD2Q3^l^!ZTvZ3xo@t+Y!{+>Xc{yw?DrxM>#_u!T=hphEHD&50UtnaE&zw%4NU8SEy+$fpIg4~9?P4Ta3qPSz@()gCoelyQge_dnx6qV?{FPoQMiwTR>nFEERV!EDV11jUi@=t%~{{W{MIoAChaLjsd>es6H>*-&q^jFllGWb9ZBXCe^ zvJkO_apF>{2b0vxP8wQWM;@1yf#rY1s0C*bOQ5bG3elIUwp7V+(Z;|Q%(SRe!_xx( z#p+!xL{Zf!Bjt)*p!Rt)BvQfjD0QVwnMirUJ+i6caJ~rmqGbq=5tU2Ie^#1uPWqn_ zTOPGSV*X>exAe|VFZ2foG(ek!0Lv+sL@evlfgt>5H%wpm&kU`1< zRAj*j_iZ--BgTkP$C;`a7ZwhP69E{h1WrsHN9|X;i-op1XTKNU`}mhV{{XN4Pl#e` zk8bAYzYoyfI5@whewXV$n}pXF=)Xn!Z@G;wsD|T6$pHPch6_6wqP;N)gK&Vlg;vXy^MlzX* z5jiPOm$oadR@Ng&UwPm8pYJ?AWwDB_zeg_W!u}xle?s}cM{}>Cd|x>aKy8WXYiJ*U zJYS~$cj~*q@BSf+4z`4TNQi+Eq-F5%uQ%zwE??01$p$gx@<0T&H3f5A0a)vO)exRKq$mGAYQAy~@zn@oT;KZUH$MfG2x9rbBv^#gab zYhk;36b8sU_E{+kl~HOiCD~#GvI%%F@+wP1zYJ3WHSSHb4x;$o1*@N7h@?jeLkxvG zNui>V63YC*3`UD$;W*L{aOlMYGSf^;q$MaI%o>b}jZ{QN23y?U3YCeOaRxD8s2ZIH z$-chPN=o5KIvKu1clA%8aC*)UKxpxN5Cy+09)mOw@pA7I<_RzV01MB*qULyG z=&k;t*ZZ;SpL)mu7OT)p>?HL?{j$5N1Ayz!`4eJ5gRvndZShEjVl)OP@O}7~`YZHj z?l6hmiet4BmNJ}?-wag|E%zX$fAokaj-s?-iZds(E}<*In4S{D-%*?xTd`tGYa?BI zLS)*O7ERk<;rf>Y{0YS6JPZfDfo|%H3-MpZfz*}v9CsjTnCmc&Fu9u zN^1xDvj_FsBJ%ek51qZu`~Ltl+y4Nts4NxmJifwty~pufJb&A4Ne$!J`w5;V^#0f1 z!@w&+n07;UPwf@2IUoDP^~7-e*B&nmx5_LhJ$&CjevC*nSHav0`SSiV8qoNv%f1trP8`djmUHx+v*M4)sx_vLi1EV z(h8EG{&D6G=YI>Vr^tEt#~A#K*Itk93};V_92{3Sm<~G_@9!F(tTy>LC#z?DCpNhJ z!-4zO7H3b}zb-C1=cn^x8oZzSVsFH&r}_Sx0NC8g7leRL+%lao zz;Omz6yu_xsE+K2h{37f>na~W2zZ^kO$Ga-&grkYHg^aDE>}A zwDvI#QQ)OQF#GWz+)7mrP7=KlcK8(Pb_&zSQ8`uFeiCpmT` z@lC_3+ZZE}m9xgZDrbquB%|!YX+Ia=!^J*5>5Hj`DRENGo)W6>w(j{`gBEXnp>s9% ziIveCmdiU6r)L2DD)_*-UF`XM{#;hg*XIa=U#1yUvs$(~{{VI}{10V@i92}CuEyRx zgegMzU*}vKeRtybLLMj2ssMa|!=IFYD;roERd>(gvLV*JcjVJLuc8fY4MA~QF0~BD z8UFx7&ok5$j;-I22Lx0YnEvy;vD*uO$zTS{FP^bS`?cJBJP8uYKY$q6m97TT7hQKG z?F6qu=12<{)7;Osy3hWfsn4M6gGkasL&oV7hiYcX-{nfnA%-?Rsb4o?{{RxQ9%$M9 zJ&!sHgE|m>P1ctA)S}mQHhZB007e(Cccin!ABr&*gF=)P3>Pt*BY%}RFi7L3WJB|! zy|oby+;oTX?M1cBelm;sgi05(y&cR*z~kY9?^zS3Wud-I8e`J*#oXL6K?i}R6uD@W z0%Dlahzf~iO3f7!Tf>lPC0ASXF%cD50LJsFLmP_>$#;k$6rfDCac!0~h~bEfEx5)} z1BxOe0MC>ySGscLm_Ne7`fFQ9@_ zX@DAr;0g^!tZ1DX=0#I5J^>{GUSg4eL;@fPh8qcn35CSR)wVGvXHX+iLFg;jT1Zhns_9oc$I&pF}+VS{{S*8Kc4>ZV6y?*#L%Ta`%ysyxFcF1@K zrT+j<%}GaZ=L6|1p&L*>@LVTETE#~;nx6RIQo$*0dQqmy?+YXSlKqTHweT^7x5cJx zKvE@y7{)seQH{VrtgYED45B=&)qG5fnbsFh@oUoBBPNG+@B$)~I>IVcN52AW)>pOz=^h|rKR;X&DvgGFC&CbpOXo@rw zCGF}3D%KWla>JY_pTU1v_%!PM{IiZ)-QCM)Px-#_%zd-}0JJ~Xe{=me{e?ma6CKlK z&+|OuUx#PUQO@yyDV`hej`{xpn;s85$KG4vAIP}>0C<}J08|LC5B!eccPIY<5Jsb? zCunznyXHUZzq$Uq{?GB>_8`1cI(I{O!{zhtWAXn0uO7Pf54rru_nC5b{{XS(Si%kb zf3MsAcaL8BVPA4%MJeAmKi8ph;@1={U7)oJTP@tR@p!m91^7p#1J~aoecL}Tr5U#K zLmgS;Fyw>&i~FDGKkWYiMgIV2`Y-zx+V=|tayYqh;2k!;%(f8T zNN~g$fYwGXH+KS-ph`);E?)ySA{AlTGmt`IxGGjGT8kH%wAx&4j$T$=UMdA}`VUHa zFVlQ#3t8W!p&waMsNwjqcM(v4&y;Et>b z?8LF$zliQ=UuFGx@};m0o@k_g6>qpS_f`U^p>x%lci%)|`plJkpZ+I7w+Nct`XGqN0vV^`A)k#1RhBHpOJ>nuwmpmFc;0 zLZEGkP}tnO6*o~TI3PZQTy%BuKdf8hsE10$+;+f!DTps}TW3r&!ZBt=2}jr?Z9^UL z_cu#cH0&B2pwkcbQp@iItXG^xZvOz^l4lk1InA^m?BW2V+xE`pjY@!|`Hkmkw|f>?03u$? z?=&@Vlzrf4C-Yv9_+B$VpfmpfkpBE_o(ukmo;gInkLSNc(u{lz)c||$V^8ZBlT)$N z^E#K;Q{fik*8Y_rk||mvQa#|FZJA#N*BK}u(j8k~0Kvn(aCPd5Z-TORL!sdt_u^N{ z(@HYJSo{Y{uCQ9BILR_cE8VtF{P=TeQUYZMDo^5`O-F(kCJ;W{H96M6ptpY)F| zuq#_{ly$qrAvHem@RMxnyUuGvA=54URf1F~*XIxwR&stKFk^1Dd=q#KvkHGcp-Io&I% zmtKnr)3m-!_a-W8C&!R1v4Il7XT(eJ=u2Q(J_!RHA$3N$$d@FC|(3LORwA7V83(c_**UWdw=&9_PBNN4h!b> zW35zNc#8wj0UC)1#LW30_59;#Yx`VMzpY64Hbh(3%XY6CV81-0Lp6ONK9K7Gg~ z-e+}n{MIe+M8KbNie2}@?R|cDKdh`1k(obfVFsU0GWQ%tFsbozikPzy>$$SE!o&-0 znCdGl)NF_`ds2<8V9K6lo@PACmCeD#970|t;t!#ZsQS0S;-ETM7jY8C80e}cMNE9h z&(J+5p!AT32PlD-D=Tc^Aj^EP%w4{P0H z09PLTZN1Dd8{y|{Gxc9azhPxuZ2G%?-|hpiGLW_TnfeTLoF#GJ>2`w3Vvc0#a6)zK zxpAk|E+Qz(7>^f-yjT+8%ZKW(GRP!DsH}&EIESqCj2=jGLL3|;qjJsM4y|zEB|vY! zh?K9bVdaZ(&F4npm12=@SaEXV2*UZ7YMGWB-#ta}OYa7L*tPBa zPk;CEGMBk;&+{+e{r7`X>GAwc9ys^odH(?H+}rNr!L-RL>HaY<(oaLKX4b%PM8!REfzKlH(hLmo1OE1m)C!?-{Q!vgm?#unPY{RK91spE(-KC&jJmWBqDG@ znZ*&)E)v1$KDEpG&nk7qj+n#QiFGUMT79B=uIbGeGQ~mwU|bJL1`y)Uh-YN8G1y?% z<=nRp$|5JC<;&na&vaX2DH6eavg{Dz<;Q@qTX3pGjLdC2udtyi7U3HGYHWBNpjMOneuuAwpiP#O5zQq1M{-n z99+3_;>Ir6Z3&>vk4U$kE+mXCW%wXdDWU}1)BF)caA588_ny}-rn5JhzklX#du#aq z=D*c6-1q+ge-X^zzwmQ$})vdLZ;39=bwkslf`S`l2_xp89`aq2PxHOngE+!}(#cv*f3 zFt#!h1_5+F3{kCi1q1&8ylc4Q{akO~`Qual;(x!@H_P-l==tXOe?tDre!qlaG&+M3 zPHX8St_;W1rZ$U%&1shy{=vUe{Al!J(EWvZaX;KQ&m zHaOXc*mo;DFWk9d%Iajz{{XIBy8i%I8-LFlnmm6TZeROx^zSose0x0VTxO1X`aOLo z^l#K<{Tk-^nT{ru*}>>E#~HVwgTUmEBQ9l+6)W{;(4%o}mlj;KJdlCxgo$65piole z7i6%+tBK%nQw*;jk(717%(6la1rb#U5j$C9u=1Btm6r%}a`__KCfS3NfPw`Gwi97S zk&65$&6Nsx%R3-lK~14SfNiKLY8nn!G?{lQ@h*f4dPhF;h*k^)r^TEHn%nOMD07*< zHJyJGt~?V`{@=$Nv$;Xt7Eo%K~6f4q>7@$o*o!N41EL= zJRB$Nuc%zNf%gN4nLA_?xIU5;@wm~};bx$Fo0Thr5pRU6Au9+<)kd3#FSZn8hS`3b zm(HOHZ4k)*+;{7409>;cww0cKDriuJAnHb}M6V${0zC=$j>vp0_Lzk!i-C+fF$4n! zUwB(JLB4FXydGU_w^r3`wjgypzjc# zTuUEv3S4dK9KrR@82Sc0jJrQ)K8s?iTp4oMc9xF=AY8Jppn5eR*<@7ULW&Z$NxpmI?)XzHOTyAaV z{RAdoM*je0ew*}NCxqY`=DBw_^>jmV*QMZN9$g-yFYO1UQHyQI&_~kpC^s%#xooy( zS}cw6Tw5=jga{d7zy{_zk1O;92Y>+q4fRqMIlf|@yj&7c?&XBFj;NcA)rEH~P!r1* zwR|mQf>?$4Gai0JFVU_H{{XIB{{VR4{{VOC{$k7ZEO;eO{(~>l{^al1QIqEl=jan& zx6uC9`pAc-lhpkqzHVXS;E7|hSr`T34@k09#J7w}mG3LwAa^@rRH;im&<(^u=wq-m z+MhFn;GZmItXiu?(e>N{i+sl|EX<{XiFty*Nd4SJ&$`4kJ$s(hUZXy`KA&H{D>n;S zZXHa+?a8l6-vNm4_am3pZ=jSl9#5o0QS^&1kd+0(AqzMV2q3zGd!G5=L^6nhz={RN zYM{X-%s5(#VdsjS!EJa)gB2m;mavh*S&z39Jw@zH4MEPJEG^L=WG}EI`4=<^ey#DZ zZa2ob-x}h*KK1o4oAuwRe?fd-(!W*u9&PX(6szsUucD&{3=mDYeNXMLtQh(pNc|E# zU!tIKaCxmqa;q#UT7r+6hY2?;QeY77OC=hGBEcnNxT$Lak9(VRq!C0Yid#fqkR>Xa zg`pn_;%Xx|aF~MD>K2z`CF3XuLlc`7OJ5KH9n&s>0q1l1v2*FqH#P0e^$$j)#qrnb zAJ}h;m-O;qE)2fj1?jkNhm-q(%j+1gOUWOkUTRdbU!u4ogMnFN%s4n69>gOkR!$_i zLQ9X9K4`1CpE9D4jtFA|whHk&Q4ox&TF8~8M`Wn!prRHImQieZJ?;t?Wcemf)1j>|2= zMBdmw<>C1ytd8GR@Mr34>%OL6>M!+<#ta!2KND=%i}aEG&<5AT=s@Nnaq4mCBl0RsXA0|x>H1OfsB0RR9200I#MArKNV zK?ERC6CyA&Kw)tOffO@BP>~~2a z5GaY!Y2Z^4FFPV+&x1$$bYD}hh}`nAsVW^TKL5D{{yQqSLa*mDtuO?v zDpbqJoi{=Nwj+>>g#8`zp+bydMkm%%XlU&mJ`perj-^jI^aGlF0jfNK-BAFo5HqhR zRK&Qr=+K;V5_yI^GbQJ-?JyFxZ1rULRLb?J-9ZIfy`E~=r;i-o^G102EOz$Vdp<4M)h;tNl zKPn{_#xd)!FqqEFN9rT^RUMFJ8t#ekPpa1tkA-5c7OIWv{0UDo=O2Fn0!RT=id4lh zuIpT3LWK%B1&X0lGhp&OkR&eFH3(K?tPRt(cgg_~D7HLP=;^Z{4{*N0IoSlAN+)ys zIOd*dQMgoKG4A&8Xt%Rzs_(LUi;2W@#sO1yQkd+*aetQqEUW?QoIA(dV-lRgJVWTI zQvhdp_z0NqyHxiV87ThGpz(A}y$2N^_)TM;UOP^D8C1Axm2abZ zl}d#=UQvFkCsbPr^xFMGa~#iv*FizR;6fw8r$oNy!88CNwh7M+5EVz})e}6xFD|;G zXZA7Zp*jt*DoD_(bsaEntWmc3LX2mUF*pGx>9U1PjU@*XONx+Ih>}$bnS>oeAywjO z(zdSX6TS>}be~kiN8M84Sx)Zn9#Y-HDfEH3u~1v8Yp^4otu8PEWfr#Xx#*8q zCso)dzXQkuw#st;%5?B!kn*=&VH_!o5~R$6n(*s6Wi2uh8HBf0O_`f6Mwhzm%Iw|I zWOqdcMlhjeQFH_+Uecop!-C;Bt|p)qDVJUe*nE}sAH{IB-^0`yV{y3Zn%6wTK_yb) zx2gOlv@}2vIm{7@qQVD8HBhHCaiRMZh-P z!}bDs#sidJkZ6V``zFz-?HE&NJT@VA6-LOh!%&*%H(P`Nb=rIqoEqpebVtG|qDrVC zs0;{H>ACKPf=b|ij>200Dbhc%ANExmjp3Ra&;>9w0eyFl;5f!Lfu;2a>J|-DTTQI) z3wTfpEQJVwQDGWrgU}(!w4p?sxWG+lXOw-_f^|c(bH>P$c|%E72ne}F#cQuQ^~1mj zAch+)d0>%bB{hzA)crD${gmDroHGMyTXOPaVYZK!*|;{i8(W%8sUF zDs@`@4f-HK!aM@dQEG%~XH`XNTU)42t5J^3991qYj$01Y_;1vaL|Sfbwmq7Vq#KE~*-7l7h8 zvNAWj)UFrloIei`3#xTG{g>1$GKE@n>ItMLF*=f}eNM5aXi+&Bbvu>4&?HPCa+yUk zX34d54nXuPFjgqQ5=a38906&Nh~+R)9@tN^tHUwUTirkOxNoOauih>JfZhl${uLjk z{W_-(Y14c2e{}bO_XlOheK&~VEwOet{nc!%Vs*(-$lB?@46&&^QXqx9K1 z^r%%XaTM~iU4r61uUt9eomV6&OX1zrhQS`Y1otqunR;s+6iy zc3O=fp-k#mbZDTtSR#2tcPrzV3<>G#!aDjJHsoJmq?09YfWTl7~n<7zH*AlcIa|gvRG600onU6Hr>)!l}cx<257l6D;9;r5dhdLCPKO5aJoQ zU9(Pw8i%ScJyv8$eVm-a(yUaVR}07STpQ+%LI&evsC{~Y;osZSc7B6GxGp8XR;$Gd z%xf4K6zsT~RE2l8i>8=1C{t@woq@&)Sz|>76VV-Ug`$qUfM9(#t3ao~9EX$3^1aTu z34JU=!k^i$3P9&Wloa^Y9ZnqHx`&e6e1-;Hd6!;iJefvpppWr z?6TC>m%0=+umY=7^o$zf!yu=%wT5Qd{W_rMH$th?Wmd>AcL?zB8W4>-gJqTd3Wuz& zJ0|A`EJ#nFMZyDFK{k;)Ax%`NQN*(|vVveul`>mRp%RV+s6xsKRsj^p-vAqKI#DQHE@g8Fm#hZpyvdM-AZqr}vw5fN{)l?6f{ z0S1zT0M$9&5yvL(vYmp<8lp}y&nWsZopOPN@r>X+4KKBq`Qly;%$WEncV6qdcu+bGOR01QCJvgxB*TFdJe*j=H9Dy{* z3GI+Vreu6z4Mdlv)9@5@tov04U6hU|1&-ilW&B%ByC_ zUV2vwuV@KFv5}4BAoV9ee9a{yFv~DI@iaG{D=LOGBdWxeKuOMvxz=b|i_AjW>-#q+!YTX}~7PS&7!@ zHK$gt&ASDcp;GptTz;mk^v;iumBM{yp90EFaj6k-=Klcvo{Q>r>G3>mYV^#p`C$Ek zO}RmmRzkZo?z=x_4H}TMQy%EnIsl8_R5OX;HuFO=H*Wh&$#*r~-o=gHJ^r zNC=jWrDy07E-DZUDN+&Sm30a|8OBs1FbHxR3YLDjA{{SG3iq$!=i;$D>ob1Swi^2f7z)Yqat;%zn z(Ilrsd$l z1r7e@pulu+MWJ^_ff-pzSqjRa1$C7|kflnJaF0k7f}uwwBLu=dIYzvr0Ed7O&?>JD zFbPJxi>}Z@WmZn5zbjA?hTS}a1SXZm{{Y8-*-`x3f94UxT~nJ0&e$THqebu#TuS1u#wouu`NA zKtH4om)Td!Ydb8(qHu(C?*m7prFm&Z&jL@I;5qT$}xFoUQ@Eea!a zTqAWl1m|FC63{aKfG7A)yNscXViw496hyBnf{H1~;D2EVm;|BVjzND4o`F>`UGacZ zWDL+B;;0a5ACj1<)8XGyzle5PX3jr02kck=Cc7(C4k9TG?S9W_x@|^3LaJDV(t;=z z#8!81?4}~hk7(d)WEQliWOyzn9V!XIugoN;kJOs-)tnvBdm5bb{{V$Wl5;0GG$S&h zvt#!_PKvV#0m_TAymU52(Vu9IKNtd~GW;t%v* zxPCi|rqM&RCY!C5*8+NUPNBO^)^j1XE)!hWXi!91J=q%L&@ERE+?1Nlz*V?l`?pl! zT1YrC*-F_txllk7Q(2hC!&KUo^wJaMwrP33|N{$^0rZX~DR|Ei?ghOIR0#^}8y?}e4e zqIM{w;2v;25DKQvxVJdc0QFR!8Bwkxz^ZlWRS?%-qG|0w{A294dxfadHF-Kn=>KVWvmw!-8&pBAb3yS~Y3WszkwKr{WbCK_ynZ z6HWo`rf#bmb%a4qpy8#~iIwX!N0i<=u+%i8$GqysuF;Nsxv)Kxo-}BHfirDYHa@#z zvpyZ6Rd^bVyF0`F(FLXmAf{HT&nw77jp7F2{g>nGbZR_hbGRR5>a`!{c`2=Pj5HEe zE(z|vLj$Jil-o_YSRp-u;be132(ZQtBp^$i1d?Rw)1gKuyP|8N$Q%g7VS8W?fK%u( z_t8Fs#+L|!H5~a#Sr$>?6V6X0D8TnxP9p)z5b>HN!4qOr@p2{_6Eyqb^oHm9jRy56 z{T0VjXyEISwHN;Yq{9R5p6R54Om&qJnqGC3_LFix;SPuSPHRn`0ry9SqHt&-z1vZuJlF znZeL*#Qc;R)`(~Tayhw?%)+d2?V_Gh4YX*Q99RpCoz4w|ZP3IhLy#W?gyzaL+IB{h zJ=RA1qQEv;M@0%0DCZ}@frwFy^=o-YF!8C~>Y;`d^KpsQIN30F8jg$U4kPL`2s^v| z6~xsCf#QR%O(#e2zd`v~yO=WcUZ@)Z?5b4^1VqSLvVqYx`g<(1Bw01aBx%txz^TB9 z!coRBo@cW#M76qG2RdDsQ#u7sLjWLy(GeIll<$;hSjJ4mVZH<=yRU*mgj%6FwpkA9 z5$7)O1|tE80I8DWfJYhdhl5CYiml~@?=~Y;M6DVEIxnVAU9nE6zhQm7@2afMayruPUfzLAvu)gJXWlt3UX zp*K|9kZgz#;Z728h{!O23#CB_?73ANbdFqJ9DZOCva>YDF(C{-FcNar{da5xwUyljal#TbkNspSKy5h7Y)VKp>`hW)r3n=$U}-{cDk(^&!JhZ?upFOdI{21 zcT_wfXBETt?*9PLYwy${M~Y)2r2|hhfM`0L+X1S1BBQ|s9)%55-7%9*t_MP#f20=5 z?hvW81D7;{4F!3lek)q(h8cC&fznOGv5-LLxrvi2<{N*c0w=A;Wro=LD9S} z)Qv=;S%-M}Q_!g^F*_+e5h%}03xM4hwHkw>FC|R)#ST!Pcl-`nQ_eg_32+FQVJe|T z6i@^-CVA1>Zk>fOVQ!@`-BROtsvtIs zyhAD2T&k5{Q_OzKohQ^b7V4>R?5A^ch$>MX6O4J=Zn~=MVQ76gjn@lS4ny`s1LpZa zE%_#Hr5#a3NJc`KQX<9*?VdN4+qjs3jZ!vUx3cX?PH|a^nMcr!Mh(>)mmWjlj}nm7 zs+MR9p}Zn+I8hTNJIru$D<)Qhn5Kvbtj=Ktq=F5ITnE)nI`hN=;TnYQB86@df}KXW zuc$6MlcMAPhr@AgH5vUnx40HRA#offeh#5+M(McGKSqQu+a^#0y;Jx`uj!@`ZRD9e zm!4ZKeco$x4=x%A@+g|{K}w8fe}>YKt&ZyT&gu6MZ%*D7LZ5CTrbyI#5^~Mif)L_`bwx7_=dtl*dwI1Az`Gmp<;(qWDs$0m?{dl2uM1n=|=PNxjOaLT>9O zwXF~bf*sX-r_u$%(QvHx<#K-u7hzAR;4;Bfc!JA1;Q|~a85l+dL?;U(7U;AeDNb9X zjHyS`1UU)rA4WNvB!HT$J^{e#i?#|fv-lVs8RiRD1a3|aR78~!)K?2zh+}|bjzDCC zqJ2Ncadj&3b=uZ}HX92-3bzs;6zDT0CYcf?#Pun)Cthj7LVa*7+ou;e$tkp)?}WxR znguFwoF|w&2R00Z*dkR*OsFm|)l{VFX8JB3sHRAEg-qOX>Jx$?6zRa=c@LzLgREs| zF%q-8s*+px=N>5Kq%KL>5aE=AZPYQqhk<*hP+^|*g!&{2Y0MD>YMXS?nxIu(a4{*V zopAwb9hKs8p}9`50WzL*?40=SQwRV8EC)rDgwjF|o>K4=oLqwolW8@&_%f%Ju8SN9 zEiw>zb%v<45eDh>8S(Cbt3dT?Ynt}UZ;85;<9jDI$;k2?W7GvQ8}rC99tiMhh-*jP zRqp~8cL@NhWP<73Kv4lmaEAnGGixT&a}0{EehkeKk~LL(=_EWTz~roQmu1srmT zDc0RcAw8lkiRC_=A)|T6AjFIZD8+*_fIz?{R4~*6D7nKzn^;Z4ZdVHwL0M-|jS0B2 zbsCM@Q9!9uiCUFOguqXHoQ(UGU?6i?qRxr{L@cC~1bEZT$4HIUh)xBCQ>qP$Bck$u zkNQLc6uF+^10q!5V|AdEO{EK)qR>gYEHir|Ao{Ssx8sx`IY)~~Se?z2wE!IzS4o5n zB4rYq+e*qy+9ekjlq$EOLG3BUI0tGFV`d;DLAw;-SYY8vh3^0-oPbkir&IuFkJtf( z9V0jLp4`GwmYXVdBtgT-V{Mb32f0o)DV)Fp4xuu$9j%cQhRO^0BBJ7MVD?H*;1i(~U2`^7FPCcXBp-rw`ViXr$cYzDEoE7V64I%@LU05@(59S^A^!kkoKxxMyLD9SI+@Dt z6^s#dG)587r&~m1qA-kfx}Quo_~bYR`hNVQ0f`DQjA=Jle+k<)RV;DJN;HPW7PumP zkZEy2wk-u7>jOds-O6#7V&Z_3njm7<8jk~Q)kg=oFnL@1D^5}h-bRI1 zVAS+PMuZhf@K7=(R%{%@L#k_`0GtC)u|de9qEQLnloAz+j!~m|MI-3;pvT4~$Z+J=~6O5oUA|S#HbF>Swe?{0mb}mMXmuBUVs6H?xD-=N(x6AkC z9)Jljfwcbs3C_yWO4Q0Y5<;0bRGU5GoO2vpQ$(3e5qxPjNpa9D#-oeR+Yt7+kPe#q ziSZijt_2GiRoez|YoRoUv`7i`TOhRMPnJ3j(<(K>4U`E@I~`GxcBa2Qw_S^!?7J2> zQoDmjqUUVY-Z!!#-BBLpLF!ZPXVpl`9#U~3Zt_e z1`aY&XnX{Cb^tEvO{UXP%`*XZ4C+*65D~g#R)R3ORZdp6lXW_+00SC6dcQs?qzyp% z<*4G;7X;HS1nx1ZQy}*`Axkyt5=t~1`bh;-d!(`2j}eZR1JPD)b|+y;@L=wz`A!Ep z%upjpK)IS5bV8sg)g;7OHs{?_tY6)6zxz-T!$6K@qIC6Ajrogt%Rxn?a)%b6SbdHI zoN^9PgKct&%00?^@`xIgF(m~_u`Vc?GMV|$!lQsqYu$N-9a5ARU8otqR4YDQH8W(| zb%-zRVihWmbI5(&)e-=d9agNCoULEgW#=$d47#D}Q{EWaC<@QHc2%0CA~Tt9TpL!S zrK0YX#?%H}`A(`^RE?s+WvuI$f$R`_>9`7pLETMCmubq>?%JPwHej0I=7JG>yI|S0 zDujkOgx9^`2J5*+m4(CQQ+{^N>Zqxe(%~Hw1z2g$BG$zBtAKVGmP^Cs5*i688kH^% zP>8C_he~6^#D!Jmg^+E!{;Tc|P^T+p$3TIpQsCtXnvA^W2oOLaN}}%ZY27y9g;B6b z!$O+iKvjm0!cn-}z#XOcOg7J`T)kS?fl1I{^slD+mC zCp%J6IgH{;xlL|jo^}{B^PZexJj;Zp)uuO3t4Ylwbqax+VNs0L4)UuUtZgx=)i_0V zM@3x4G{Kdv9@tT{>inv%9jgPh3%94$KCL$}iBwF}YM?y-061&JeX!y@t@%O%f05q> zQ2elu{D%B0zn1TXL`62?N6k~^R{Xanc^mLCAD-`yO4rKXr2!Z>yH&$tt6kLoaI9?6 zu`9Kw)f%iKB~kL9oB@Igm~{1AK!*d3BeHOokb)|nN{|Jts&^bh%ueW`WZojz5}NkD zVI9#RB0{n^<|;V|#&LvcjgbUmGzY;Lj!?JCKIzW>6Wy5OD^(YOrz2x@Flqj?)7>bH zuVFeYt&J2npu>c0)Ok7sDs#QhPyF_LD&lGU6w@sS+X_Bt!gL`+dMF(R6f}R% zAN-HD97mPEI6uhmg+ubfKc3%(SIF;$OY+-=FFj9{Tk_nQ)j;NI?RH^Xe4C}P^LZCtIQaY;?S1PCT^qGUb3%gs$V zMB>Alp;ZJl)=p>-`by*vL7h$~8y)Gi=Hk-%z!PO-opkOJtSQ2>O~2!pSK zJ`D0?9TrBe$aP(b*rQ47BgVyFk`P)ibP&227tMx?J6bKObElLdGJxWXvtuh%APFeO zs!?(-OlftMSY;5=xkA7X+DYUF)X%cwMuj_ET2uAIbt%}DK8rHUv#jViDIBf=!Vv|g zF&K*+OPUdJcB*-T0e}+ZUz*M)6}C8CLKX-TY;crJrnb^mCo;jhm0aem2EcmB^h`g| zZ?l&Xc7l!|!hRs9VnPUyN))Wc2Og`nC{-v{9T9yuh$RpP@_|w@n=IiM5~Kw@z!hML zo<@l%H998CH^R;Atad?@u~}e2D~D*$JuZRF=ew!(GviM^Gc=q!6;kxVJerPha0k+i zP5~SwtjHNsrAm^eNmI%ZcEgYa7_8LlnY71L%12b{tr{ZYZc3zb(Ww6bvSpiupnW9x zvZuh1s2OcQjp-x%5R6U(mAu3v5Cs5K6_la~P_mo=LIUqBgrdS0QiBP(Lo0L=oaeh; z7GHD#l~=pNvAs_y^b(~XQNy1&{{YoD^4yL)ppUM$2MxJKoU(A(E~t452)Ke)ZmTd^ z!q0scbk~8XOoxd9WIYjpV4`{yMc|^$zTp<1vM`wF8UoeCp8Ffne$+~va&Z1W(85f$%2-fz=HW&?c?0+mk_*X@EgILy;tc zN|Oi~)df$^)NfJe-1x)Pp96>UpYbZ6k;&WgpHu(D05cH)0s;X80|NsD0s#XB00000 z0Rj;a10gXIAVDxdQ3Ntj6Cz=8LV=MpG_e#TVse82+5iXv0|5a)0KZ1@$;YED>3UCh z+|`NJ&$*!@haZ7*dGxQ|-MjEbUhm)6tH+@o?fOfe_Zt&yWz(EJau2c* zIZ%^)oEL`6^%)#2ErvX=NqWmNmLi4udndXREtXkgR&kpjs>Hu&`((WbB$no^cjp8o(Dv3kx}NK3z;$;uVF>{0A*7FlJN=(DRRkrsF#YE~$Gl8S1W z^wMvAiRfPOC#K@<8h=$Ny3|&_NX|1?7i$;lT@F`>#ZZ^$I#JTBq)^i=eYKk}zJ(G( z7h^+IEGYbmP~?!?d6ZPSHFO%^kvZwRjGAVb)4%v}dOGMxM5=i`nh)%^#}M)!OP`57BbMh?jNZ ztY4OSyvq$ycZcZNWu7p*bv0q8u1wOLHDae6H}T$vtl()$Q%0}SC%ix4o}8fRs%Dqf z>SZSe~3U6-imG^pcM7+)7btr%clN*B!N2 z{{W;yahkO47G5YwuGY=oc>KlTw2$2QMSQ$<7QC-PVUM?wEbDz-H2B^8_$N8)$x2c* zbf>MoQu#Erbo97Z{zNs4EWYK36O2zn8k^St03<$(MZ27v{s`%5!Bk?BB?h>{Oi3>5 z$36G?Pn#k2idqqn^d+ENbiN=`G&>00ytr&r{Cd2wGFsl1evbm(s!sId0M8 zC}o>5ri)^cdEXsKZFWX&a%=rubXt!@n|^!|`l&fNT6^(Eo~(60hAz&dlEvMBm3!=j zt9QHZeXC|wmD>AfsSP;Xnnt(P%TZRJ?O!75o1~qZKSwTgCH{!UFiAyH#db;MDNvQV zp(+|oX|D*jTkTP7T5HQXQY8qRl;NW(L+-7H^?I&txlyO|vz<5lVrl6^NmLj6?yNp= zklhIz*346DX%f2P#T58sgF0D7B91r{PqKPY)Y?ea`gf&IN;mx)lX@C)U#a`0yYohj z+_a-9E!l0A+hzCj`2KtR`YUv7$wq|e{;>1)QmqNN?Xjl`Ip|JJZ!Nxd%6PukBsxNA z@+>uIR~?@PC*X2%XX+!WCB(E^9dbr_>MX52HssV3=lN!wy5giI*?oz2UH4uWy03h= zA@P=|PL7f8v#SL?DaHI66t!b~^{&Jv*w~PU?U7dRx=X1FE$}$Y;Nz8*xv=ER&b+0m zsVLVHXVR+BhA%fo*p?PA;Qq__68zKcLfS;OY2%bKFTmA}e3UpXPungTtMt!LTTQ61 zxo*7TyEiWtz8=V{67OoaLXu>o)kj0JtDM^|FTrJ+x{LK@x4x!*E4>JRyjmMK670NQ z7v|q{IgS$~tXn+NHIq=03F^Wz>u#K?xx!`P_AE=n60Y_k#WJNHH$s;Cn=!-Saci~x zFVm^YbJylg(xXZdP)ZHB_-yEBNm;V-)N&+IXtMtR3x3h}cp&$ot+G^y$Y+F=?i+VU}5C zo0TSXl#Poo6eaIbUe)$2KIAXC9!DkQ+ralCLHNS^mKqwYyeuW#MG0rXviq!4I^lo5 zAMK%ZJ>yh0V}tNhULJ22i!9utk}R^t?$&6(e22;&cflVZ_j=KbXt+`Ss_eeXRuv7!&axgQLUDu$>_30^6;xxayl;k4d35yF z#mJr>;&@E!+~l;dv;7GT_(C4%`C*m&cJD{jwfeH(%h>u#{{Yj%zn=BZA2`_-#7TCn zFL?Xcuzmjk;yzHug~{dO(K*LTE^g@xN>J)f(?TB1PYn%WEv#L%A?$^C+}W%&Cohsp zo@k*gYwxY7{{ZEk*T4jH`LWM?$No)LS^nQYZB(j%7pJb(OJ6O>hdp^ zITwXx#q#rcx*nX|o~)yBT8@(Cnl<`SsMqoF=qIq5CB72ucvxSIe!NB5FSwr=)YSbb zIn~h*sD`91c_YRz3|q+$fsGNBJgInOnP&3-j+9DEgeI9tq;#XDjy*{aZHq(2jM3Nq zAE}+nqUP*vH@6;=-3f2n`xoF@A7=ZaM<|yh{A24s?dSfDE~x53c z?k*^5h`hF#k{o=eOp~2vE&0u2YTglf*8c#jss8}opGp4!Z>NjzLUj9{E`-zk6DpN^ zmE@0%qx_pBxBmcG`o!PU%`f25gx0Q|@z#gneg}jtw-lBs4k?`_HJz6Nn)8N}@N`7z zMP~5TkF!}R&rztT)2+{?wV?~=e@-&--teT*riJg;@6vvg?*9N5ETM8soPAc>6EtK0 z0J#Of+=h$cVu@|G7@P2Qv1)$;m5x>5Q*3SJUx7J%okf;5%YBVsW~04%IV(zR({ONI z3Zq*uNn$ea{VO_{q|T;wGV^}kJU7_;pQl}jd?9u&Npi$%uB|B|5OLLm=X7~1d{Hh) zt18&gw;31UV^m$D#T?Qoviq96y$QWZ$tqn>c4;KE;*(GLUaV!_`)p5g{ms$gA!hrZ zYwmrBmx4TJz?~enBUk>Mh^O~_GCGix=X&5<-dKb*Y?$$OEzq`jOx)k+G|G6s&ALvV zte5Q{(|)bD{{Wk!F#R~FUklbvqVLpR;fr>UIC!sO`w+#ju*%NdGo{6Bts1XE-Oa`S z07jm?R*RdHYWB6c3C3#T?PH($DzM-F4PfaDf zq2?l#wC0=}N$^8oPHLjtX>RI-l_BB1*uLY=D#j!GFZ~vaevEYAkt!V0I;^jQu1k?> zBBLScuLW;iioFGjv!(h8KZ2RkQuWaLSh)2ct>lQXi`AlXU1zYmqP9^LrbXU!X7$%O3i&IJsy~!P^A`=RQ5)P)Xps@ zyhzSEk+`vWJeF`@q?2M%k0(+dr^k9{B9(0;#iOM?;>v5EO?%hf=doe?7mouH+p%&_ zoTI?G77;H5;T=t+tkn8v7&Q&ZThNk^DEe=ymapxr+kfHz04I|DCeq|nL)3m5QkUGy zOVLdJ&DO=xr}9QVNkaJaujF3&mhN^o@(Uzh~(B|JH6PEQtie92;{syQ>O~sLtWaE5#3k$J(x+EH} zXBtm*Msb?ilhlTqxiWuIIqa1pFmin>b5H5K&Hn(QB$XxXzlWEL@b~l+?O0yt=hnYY z_H0VtQcQ3itXQTgN9PYK4}U^YjMX%1XwEywbahEPPjcIna&5M0eLSSDN|6Xg3ZlKQ zBHXISINA3jjtehgd(WQN>^$X<;EKXR8}M4DC1qtci+?9u@+O`n@ptIzXv0(Zen-1OpMfex;USMGd1-*X%waX;l*sk+J>o=x?2|Q4J7w2KLnLBE;hCsj{9YOnZ2tUBlfYX61z57*&8#0$B4YOL?4V$ zu+{8V#Nk`6)$Fg+S!3>fsR?$rC5QH`C9u?m875EeCB$*e84gFg#JGtR`du9o6l9tu z_cDGl{uqZiT%vz)zQ#wCOYV4=5h2L=WfXsk?2nwi;)wagN@q(Y&tx++Et+|iqC29J z%hMcM#d}-yU!vc1S9dQj?LtM`WxUJ4#r85Bk7tQ-5pNb=XX78?h;xO?C-)2NWO+on zc$X0)$ky*Gc;>dUK63VoBj*wFZ`%1I$guZ9Bf7-uh~oPkYZO6I75qN2RsB-5;ajbb#L!sQa( zi|wIuJ>DHs7R+gLc}|U)OXP0EY|2Z>-I3wpY;4M3Bh0*EWvXNz8ciFbF3R_`y?5Ca z+~0&HSx9>!yC|%1j~<^V!gskaZ&9N%VSmdmvNPi!J zuTA0^cwYY5)O91N7QTD=?yB9DVqL7|(3N7c_Z8xb8=^mptiL1$mM?Wh6j4QXr$Tq! z@{~a)a5~1>t`TTVGLi>gJdn~g1HZ2xe)NaJ0 zi!F Date: Sun, 30 Jun 2019 11:46:31 +0400 Subject: [PATCH 084/211] docs: remove confusing statement from contributing.md (#3764) --- CONTRIBUTING.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ef992fee6..832156bda 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -41,8 +41,6 @@ To pull in updates from the origin repo, run * `git fetch upstream` * `git rebase upstream/master` (or whatever branch you want) -Please don't make Pull Requests to `master`. - ## Dependencies We use [go modules](https://github.com/golang/go/wiki/Modules) to manage dependencies. From d4cf2040877aea2e863265a56fe1d91e93bf021a Mon Sep 17 00:00:00 2001 From: Marko Date: Sun, 30 Jun 2019 09:56:24 +0200 Subject: [PATCH 085/211] libs: remove commented and unneeded code (#3757) - libs/errors: commented out errors.go - libs/common: unused colors.go - libs/db: unused debugDB Signed-off-by: Marko Baricevic --- CHANGELOG_PENDING.md | 2 + libs/common/colors.go | 95 ---------------- libs/db/db_test.go | 3 +- libs/db/debug_db.go | 257 ------------------------------------------ libs/errors/errors.go | 21 ---- 5 files changed, 3 insertions(+), 375 deletions(-) delete mode 100644 libs/common/colors.go delete mode 100644 libs/db/debug_db.go delete mode 100644 libs/errors/errors.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 45d1c7899..d57446a10 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -14,6 +14,8 @@ program](https://hackerone.com/tendermint). * Apps * Go API + - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) + * Blockchain Protocol diff --git a/libs/common/colors.go b/libs/common/colors.go deleted file mode 100644 index 89dda2c97..000000000 --- a/libs/common/colors.go +++ /dev/null @@ -1,95 +0,0 @@ -package common - -import ( - "fmt" - "strings" -) - -const ( - ANSIReset = "\x1b[0m" - ANSIBright = "\x1b[1m" - ANSIDim = "\x1b[2m" - ANSIUnderscore = "\x1b[4m" - ANSIBlink = "\x1b[5m" - ANSIReverse = "\x1b[7m" - ANSIHidden = "\x1b[8m" - - ANSIFgBlack = "\x1b[30m" - ANSIFgRed = "\x1b[31m" - ANSIFgGreen = "\x1b[32m" - ANSIFgYellow = "\x1b[33m" - ANSIFgBlue = "\x1b[34m" - ANSIFgMagenta = "\x1b[35m" - ANSIFgCyan = "\x1b[36m" - ANSIFgWhite = "\x1b[37m" - - ANSIBgBlack = "\x1b[40m" - ANSIBgRed = "\x1b[41m" - ANSIBgGreen = "\x1b[42m" - ANSIBgYellow = "\x1b[43m" - ANSIBgBlue = "\x1b[44m" - ANSIBgMagenta = "\x1b[45m" - ANSIBgCyan = "\x1b[46m" - ANSIBgWhite = "\x1b[47m" -) - -// color the string s with color 'color' -// unless s is already colored -func treat(s string, color string) string { - if len(s) > 2 && s[:2] == "\x1b[" { - return s - } - return color + s + ANSIReset -} - -func treatAll(color string, args ...interface{}) string { - parts := make([]string, 0, len(args)) - for _, arg := range args { - parts = append(parts, treat(fmt.Sprintf("%v", arg), color)) - } - return strings.Join(parts, "") -} - -func Black(args ...interface{}) string { - return treatAll(ANSIFgBlack, args...) -} - -func Red(args ...interface{}) string { - return treatAll(ANSIFgRed, args...) -} - -func Green(args ...interface{}) string { - return treatAll(ANSIFgGreen, args...) -} - -func Yellow(args ...interface{}) string { - return treatAll(ANSIFgYellow, args...) -} - -func Blue(args ...interface{}) string { - return treatAll(ANSIFgBlue, args...) -} - -func Magenta(args ...interface{}) string { - return treatAll(ANSIFgMagenta, args...) -} - -func Cyan(args ...interface{}) string { - return treatAll(ANSIFgCyan, args...) -} - -func White(args ...interface{}) string { - return treatAll(ANSIFgWhite, args...) -} - -func ColoredBytes(data []byte, textColor, bytesColor func(...interface{}) string) string { - s := "" - for _, b := range data { - if 0x21 <= b && b < 0x7F { - s += textColor(string(b)) - } else { - s += bytesColor(fmt.Sprintf("%02X", b)) - } - } - return s -} diff --git a/libs/db/db_test.go b/libs/db/db_test.go index 7cb721b26..22b781f95 100644 --- a/libs/db/db_test.go +++ b/libs/db/db_test.go @@ -182,8 +182,7 @@ func TestDBBatchWrite(t *testing.T) { for i, tc := range testCases { mdb := newMockDB() - ddb := NewDebugDB(t.Name(), mdb) - batch := ddb.NewBatch() + batch := mdb.NewBatch() tc.modify(batch) diff --git a/libs/db/debug_db.go b/libs/db/debug_db.go deleted file mode 100644 index 658cd0555..000000000 --- a/libs/db/debug_db.go +++ /dev/null @@ -1,257 +0,0 @@ -package db - -import ( - "fmt" - "sync" - - cmn "github.com/tendermint/tendermint/libs/common" -) - -//---------------------------------------- -// debugDB - -type debugDB struct { - label string - db DB -} - -// For printing all operationgs to the console for debugging. -func NewDebugDB(label string, db DB) debugDB { - return debugDB{ - label: label, - db: db, - } -} - -// Implements atomicSetDeleter. -func (ddb debugDB) Mutex() *sync.Mutex { return nil } - -// Implements DB. -func (ddb debugDB) Get(key []byte) (value []byte) { - defer func() { - fmt.Printf("%v.Get(%v) %v\n", ddb.label, - cmn.ColoredBytes(key, cmn.Cyan, cmn.Blue), - cmn.ColoredBytes(value, cmn.Green, cmn.Blue)) - }() - value = ddb.db.Get(key) - return -} - -// Implements DB. -func (ddb debugDB) Has(key []byte) (has bool) { - defer func() { - fmt.Printf("%v.Has(%v) %v\n", ddb.label, - cmn.ColoredBytes(key, cmn.Cyan, cmn.Blue), has) - }() - return ddb.db.Has(key) -} - -// Implements DB. -func (ddb debugDB) Set(key []byte, value []byte) { - fmt.Printf("%v.Set(%v, %v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Yellow, cmn.Blue), - cmn.ColoredBytes(value, cmn.Green, cmn.Blue)) - ddb.db.Set(key, value) -} - -// Implements DB. -func (ddb debugDB) SetSync(key []byte, value []byte) { - fmt.Printf("%v.SetSync(%v, %v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Yellow, cmn.Blue), - cmn.ColoredBytes(value, cmn.Green, cmn.Blue)) - ddb.db.SetSync(key, value) -} - -// Implements atomicSetDeleter. -func (ddb debugDB) SetNoLock(key []byte, value []byte) { - fmt.Printf("%v.SetNoLock(%v, %v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Yellow, cmn.Blue), - cmn.ColoredBytes(value, cmn.Green, cmn.Blue)) - ddb.db.(atomicSetDeleter).SetNoLock(key, value) -} - -// Implements atomicSetDeleter. -func (ddb debugDB) SetNoLockSync(key []byte, value []byte) { - fmt.Printf("%v.SetNoLockSync(%v, %v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Yellow, cmn.Blue), - cmn.ColoredBytes(value, cmn.Green, cmn.Blue)) - ddb.db.(atomicSetDeleter).SetNoLockSync(key, value) -} - -// Implements DB. -func (ddb debugDB) Delete(key []byte) { - fmt.Printf("%v.Delete(%v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Red, cmn.Yellow)) - ddb.db.Delete(key) -} - -// Implements DB. -func (ddb debugDB) DeleteSync(key []byte) { - fmt.Printf("%v.DeleteSync(%v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Red, cmn.Yellow)) - ddb.db.DeleteSync(key) -} - -// Implements atomicSetDeleter. -func (ddb debugDB) DeleteNoLock(key []byte) { - fmt.Printf("%v.DeleteNoLock(%v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Red, cmn.Yellow)) - ddb.db.(atomicSetDeleter).DeleteNoLock(key) -} - -// Implements atomicSetDeleter. -func (ddb debugDB) DeleteNoLockSync(key []byte) { - fmt.Printf("%v.DeleteNoLockSync(%v)\n", ddb.label, - cmn.ColoredBytes(key, cmn.Red, cmn.Yellow)) - ddb.db.(atomicSetDeleter).DeleteNoLockSync(key) -} - -// Implements DB. -func (ddb debugDB) Iterator(start, end []byte) Iterator { - fmt.Printf("%v.Iterator(%v, %v)\n", ddb.label, - cmn.ColoredBytes(start, cmn.Cyan, cmn.Blue), - cmn.ColoredBytes(end, cmn.Cyan, cmn.Blue)) - return NewDebugIterator(ddb.label, ddb.db.Iterator(start, end)) -} - -// Implements DB. -func (ddb debugDB) ReverseIterator(start, end []byte) Iterator { - fmt.Printf("%v.ReverseIterator(%v, %v)\n", ddb.label, - cmn.ColoredBytes(start, cmn.Cyan, cmn.Blue), - cmn.ColoredBytes(end, cmn.Cyan, cmn.Blue)) - return NewDebugIterator(ddb.label, ddb.db.ReverseIterator(start, end)) -} - -// Implements DB. -// Panics if the underlying db is not an -// atomicSetDeleter. -func (ddb debugDB) NewBatch() Batch { - fmt.Printf("%v.NewBatch()\n", ddb.label) - return NewDebugBatch(ddb.label, ddb.db.NewBatch()) -} - -// Implements DB. -func (ddb debugDB) Close() { - fmt.Printf("%v.Close()\n", ddb.label) - ddb.db.Close() -} - -// Implements DB. -func (ddb debugDB) Print() { - ddb.db.Print() -} - -// Implements DB. -func (ddb debugDB) Stats() map[string]string { - return ddb.db.Stats() -} - -//---------------------------------------- -// debugIterator - -type debugIterator struct { - label string - itr Iterator -} - -// For printing all operationgs to the console for debugging. -func NewDebugIterator(label string, itr Iterator) debugIterator { - return debugIterator{ - label: label, - itr: itr, - } -} - -// Implements Iterator. -func (ditr debugIterator) Domain() (start []byte, end []byte) { - defer func() { - fmt.Printf("%v.itr.Domain() (%X,%X)\n", ditr.label, start, end) - }() - start, end = ditr.itr.Domain() - return -} - -// Implements Iterator. -func (ditr debugIterator) Valid() (ok bool) { - defer func() { - fmt.Printf("%v.itr.Valid() %v\n", ditr.label, ok) - }() - ok = ditr.itr.Valid() - return -} - -// Implements Iterator. -func (ditr debugIterator) Next() { - fmt.Printf("%v.itr.Next()\n", ditr.label) - ditr.itr.Next() -} - -// Implements Iterator. -func (ditr debugIterator) Key() (key []byte) { - key = ditr.itr.Key() - fmt.Printf("%v.itr.Key() %v\n", ditr.label, - cmn.ColoredBytes(key, cmn.Cyan, cmn.Blue)) - return -} - -// Implements Iterator. -func (ditr debugIterator) Value() (value []byte) { - value = ditr.itr.Value() - fmt.Printf("%v.itr.Value() %v\n", ditr.label, - cmn.ColoredBytes(value, cmn.Green, cmn.Blue)) - return -} - -// Implements Iterator. -func (ditr debugIterator) Close() { - fmt.Printf("%v.itr.Close()\n", ditr.label) - ditr.itr.Close() -} - -//---------------------------------------- -// debugBatch - -type debugBatch struct { - label string - bch Batch -} - -// For printing all operationgs to the console for debugging. -func NewDebugBatch(label string, bch Batch) debugBatch { - return debugBatch{ - label: label, - bch: bch, - } -} - -// Implements Batch. -func (dbch debugBatch) Set(key, value []byte) { - fmt.Printf("%v.batch.Set(%v, %v)\n", dbch.label, - cmn.ColoredBytes(key, cmn.Yellow, cmn.Blue), - cmn.ColoredBytes(value, cmn.Green, cmn.Blue)) - dbch.bch.Set(key, value) -} - -// Implements Batch. -func (dbch debugBatch) Delete(key []byte) { - fmt.Printf("%v.batch.Delete(%v)\n", dbch.label, - cmn.ColoredBytes(key, cmn.Red, cmn.Yellow)) - dbch.bch.Delete(key) -} - -// Implements Batch. -func (dbch debugBatch) Write() { - fmt.Printf("%v.batch.Write()\n", dbch.label) - dbch.bch.Write() -} - -// Implements Batch. -func (dbch debugBatch) WriteSync() { - fmt.Printf("%v.batch.WriteSync()\n", dbch.label) - dbch.bch.WriteSync() -} - -// Implements Batch. -func (dbch debugBatch) Close() { - dbch.bch.Close() -} diff --git a/libs/errors/errors.go b/libs/errors/errors.go deleted file mode 100644 index a03382780..000000000 --- a/libs/errors/errors.go +++ /dev/null @@ -1,21 +0,0 @@ -// Package errors contains errors that are thrown across packages. -package errors - -// // ErrPermissionsChanged occurs if the file permission have changed since the file was created. -// type ErrPermissionsChanged struct { -// name string -// got, want os.FileMode -// } - -// func NewErrPermissionsChanged(name string, got, want os.FileMode) *ErrPermissionsChanged { -// return &ErrPermissionsChanged{name: name, got: got, want: want} -// } - -// func (e ErrPermissionsChanged) Error() string { -// return fmt.Sprintf( -// "file: [%v]\nexpected file permissions: %v, got: %v", -// e.name, -// e.want, -// e.got, -// ) -// } From d9481e3648450cb99e15c6a070c1fb69aa0c255b Mon Sep 17 00:00:00 2001 From: Ivan Kushmantsev Date: Mon, 1 Jul 2019 12:48:54 +0400 Subject: [PATCH 086/211] config: make possible to set absolute paths for TLS cert and key (#3765) --- CHANGELOG_PENDING.md | 1 + config/config.go | 18 ++++++++++++++---- config/config_test.go | 16 ++++++++++++++++ config/toml.go | 6 ++++-- docs/tendermint-core/configuration.md | 6 ++++-- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d57446a10..d81e815f3 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -24,5 +24,6 @@ program](https://hackerone.com/tendermint). ### FEATURES: ### IMPROVEMENTS: + - [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) ### BUG FIXES: diff --git a/config/config.go b/config/config.go index aa25cff6b..32e37f3e1 100644 --- a/config/config.go +++ b/config/config.go @@ -351,7 +351,8 @@ type RPCConfig struct { // See https://github.com/tendermint/tendermint/issues/3435 TimeoutBroadcastTxCommit time.Duration `mapstructure:"timeout_broadcast_tx_commit"` - // The name of a file containing certificate that is used to create the HTTPS server. + // The path to a file containing certificate that is used to create the HTTPS server. + // Migth be either absolute path or path related to tendermint's config directory. // // If the certificate is signed by a certificate authority, // the certFile should be the concatenation of the server's certificate, any intermediates, @@ -360,7 +361,8 @@ type RPCConfig struct { // NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. TLSCertFile string `mapstructure:"tls_cert_file"` - // The name of a file containing matching private key that is used to create the HTTPS server. + // The path to a file containing matching private key that is used to create the HTTPS server. + // Migth be either absolute path or path related to tendermint's config directory. // // NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. TLSKeyFile string `mapstructure:"tls_key_file"` @@ -424,11 +426,19 @@ func (cfg *RPCConfig) IsCorsEnabled() bool { } func (cfg RPCConfig) KeyFile() string { - return rootify(filepath.Join(defaultConfigDir, cfg.TLSKeyFile), cfg.RootDir) + path := cfg.TLSKeyFile + if filepath.IsAbs(path) { + return path + } + return rootify(filepath.Join(defaultConfigDir, path), cfg.RootDir) } func (cfg RPCConfig) CertFile() string { - return rootify(filepath.Join(defaultConfigDir, cfg.TLSCertFile), cfg.RootDir) + path := cfg.TLSCertFile + if filepath.IsAbs(path) { + return path + } + return rootify(filepath.Join(defaultConfigDir, path), cfg.RootDir) } func (cfg RPCConfig) IsTLSEnabled() bool { diff --git a/config/config_test.go b/config/config_test.go index afdbed181..6f9e3783e 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -36,3 +36,19 @@ func TestConfigValidateBasic(t *testing.T) { cfg.Consensus.TimeoutPropose = -10 * time.Second assert.Error(t, cfg.ValidateBasic()) } + +func TestTLSConfiguration(t *testing.T) { + assert := assert.New(t) + cfg := DefaultConfig() + cfg.SetRoot("/home/user") + + cfg.RPC.TLSCertFile = "file.crt" + assert.Equal("/home/user/config/file.crt", cfg.RPC.CertFile()) + cfg.RPC.TLSKeyFile = "file.key" + assert.Equal("/home/user/config/file.key", cfg.RPC.KeyFile()) + + cfg.RPC.TLSCertFile = "/abs/path/to/file.crt" + assert.Equal("/abs/path/to/file.crt", cfg.RPC.CertFile()) + cfg.RPC.TLSKeyFile = "/abs/path/to/file.key" + assert.Equal("/abs/path/to/file.key", cfg.RPC.KeyFile()) +} diff --git a/config/toml.go b/config/toml.go index b1541c05a..09117a0fb 100644 --- a/config/toml.go +++ b/config/toml.go @@ -192,14 +192,16 @@ max_subscriptions_per_client = {{ .RPC.MaxSubscriptionsPerClient }} # See https://github.com/tendermint/tendermint/issues/3435 timeout_broadcast_tx_commit = "{{ .RPC.TimeoutBroadcastTxCommit }}" -# The name of a file containing certificate that is used to create the HTTPS server. +# The path to a file containing certificate that is used to create the HTTPS server. +# Migth be either absolute path or path related to tendermint's config directory. # If the certificate is signed by a certificate authority, # the certFile should be the concatenation of the server's certificate, any intermediates, # and the CA's certificate. # NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. tls_cert_file = "{{ .RPC.TLSCertFile }}" -# The name of a file containing matching private key that is used to create the HTTPS server. +# The path to a file containing matching private key that is used to create the HTTPS server. +# Migth be either absolute path or path related to tendermint's config directory. # NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. tls_key_file = "{{ .RPC.TLSKeyFile }}" diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index f24e76d6d..df05f7c5d 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -138,14 +138,16 @@ max_subscriptions_per_client = 5 # See https://github.com/tendermint/tendermint/issues/3435 timeout_broadcast_tx_commit = "10s" -# The name of a file containing certificate that is used to create the HTTPS server. +# The path to a file containing certificate that is used to create the HTTPS server. +# Migth be either absolute path or path related to tendermint's config directory. # If the certificate is signed by a certificate authority, # the certFile should be the concatenation of the server's certificate, any intermediates, # and the CA's certificate. # NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. tls_cert_file = "" -# The name of a file containing matching private key that is used to create the HTTPS server. +# The path to a file containing matching private key that is used to create the HTTPS server. +# Migth be either absolute path or path related to tendermint's config directory. # NOTE: both tls_cert_file and tls_key_file must be present for Tendermint to create HTTPS server. Otherwise, HTTP server is run. tls_key_file = "" From 62f97a69e97262b5feb57b1c2498f0c1e0e297b3 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Tue, 2 Jul 2019 10:14:53 -0400 Subject: [PATCH 087/211] abci: Refactor CheckTx to notify of recheck (#3744) As per #2127, this refactors the RequestCheckTx ProtoBuf struct to allow for a flag indicating whether a query is a recheck or not (and allows for possible future, more nuanced states). In order to pass this extended information through to the ABCI app, the proxy.AppConnMempool (and, for consistency, the proxy.AppConnConsensus) interface seems to need to be refactored along with abcicli.Client. And, as per this comment, I've made the following modification to the protobuf definition for the RequestCheckTx structure: enum CheckTxType { New = 0; Recheck = 1; } message RequestCheckTx { bytes tx = 1; CheckTxType type = 2; } * Refactor ABCI CheckTx to notify of recheck As per #2127, this refactors the `RequestCheckTx` ProtoBuf struct to allow for: 1. a flag indicating whether a query is a recheck or not (and allows for possible future, more nuanced states) 2. an `additional_data` bytes array to provide information for those more nuanced states. In order to pass this extended information through to the ABCI app, the `proxy.AppConnMempool` (and, for consistency, the `proxy.AppConnConsensus`) interface seems to need to be refactored. Commits: * Fix linting issue * Add CHANGELOG_PENDING entry * Remove extraneous explicit initialization * Update ABCI spec doc to include new CheckTx params * Rename method param for consistency * Rename CheckTxType enum values and remove additional_data param --- CHANGELOG_PENDING.md | 6 +- abci/client/client.go | 8 +- abci/client/grpc_client.go | 16 +- abci/client/local_client.go | 20 +- abci/client/socket_client.go | 16 +- abci/cmd/abci-cli/abci-cli.go | 4 +- abci/example/example_test.go | 2 +- abci/example/kvstore/kvstore_test.go | 4 +- abci/tests/server/client.go | 4 +- abci/tests/test_app/app.go | 2 +- abci/types/messages.go | 8 +- abci/types/types.pb.go | 452 +++++++++++++++------------ abci/types/types.proto | 6 + consensus/replay_test.go | 2 +- docs/spec/abci/abci.md | 6 + mempool/clist_mempool.go | 7 +- mempool/clist_mempool_test.go | 4 +- proxy/app_conn.go | 12 +- state/execution.go | 2 +- 19 files changed, 333 insertions(+), 248 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d81e815f3..9c94c55e7 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -14,9 +14,13 @@ program](https://hackerone.com/tendermint). * Apps * Go API + - [abci] \#2127 ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI + client interface (`abcicli.Client`) to allow for supplying the ABCI + `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the + mempool indicate to the ABCI app whether a CheckTx request is a recheck or + not. - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - * Blockchain Protocol * P2P Protocol diff --git a/abci/client/client.go b/abci/client/client.go index e1eea5d4e..5f5ad7860 100644 --- a/abci/client/client.go +++ b/abci/client/client.go @@ -28,8 +28,8 @@ type Client interface { EchoAsync(msg string) *ReqRes InfoAsync(types.RequestInfo) *ReqRes SetOptionAsync(types.RequestSetOption) *ReqRes - DeliverTxAsync(tx []byte) *ReqRes - CheckTxAsync(tx []byte) *ReqRes + DeliverTxAsync(types.RequestDeliverTx) *ReqRes + CheckTxAsync(types.RequestCheckTx) *ReqRes QueryAsync(types.RequestQuery) *ReqRes CommitAsync() *ReqRes InitChainAsync(types.RequestInitChain) *ReqRes @@ -40,8 +40,8 @@ type Client interface { EchoSync(msg string) (*types.ResponseEcho, error) InfoSync(types.RequestInfo) (*types.ResponseInfo, error) SetOptionSync(types.RequestSetOption) (*types.ResponseSetOption, error) - DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) - CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) + DeliverTxSync(types.RequestDeliverTx) (*types.ResponseDeliverTx, error) + CheckTxSync(types.RequestCheckTx) (*types.ResponseCheckTx, error) QuerySync(types.RequestQuery) (*types.ResponseQuery, error) CommitSync() (*types.ResponseCommit, error) InitChainSync(types.RequestInitChain) (*types.ResponseInitChain, error) diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index d04f42b66..23d790550 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -159,8 +159,8 @@ func (cli *grpcClient) SetOptionAsync(params types.RequestSetOption) *ReqRes { return cli.finishAsyncCall(req, &types.Response{Value: &types.Response_SetOption{SetOption: res}}) } -func (cli *grpcClient) DeliverTxAsync(tx []byte) *ReqRes { - req := types.ToRequestDeliverTx(tx) +func (cli *grpcClient) DeliverTxAsync(params types.RequestDeliverTx) *ReqRes { + req := types.ToRequestDeliverTx(params) res, err := cli.client.DeliverTx(context.Background(), req.GetDeliverTx(), grpc.FailFast(true)) if err != nil { cli.StopForError(err) @@ -168,8 +168,8 @@ func (cli *grpcClient) DeliverTxAsync(tx []byte) *ReqRes { return cli.finishAsyncCall(req, &types.Response{Value: &types.Response_DeliverTx{DeliverTx: res}}) } -func (cli *grpcClient) CheckTxAsync(tx []byte) *ReqRes { - req := types.ToRequestCheckTx(tx) +func (cli *grpcClient) CheckTxAsync(params types.RequestCheckTx) *ReqRes { + req := types.ToRequestCheckTx(params) res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.FailFast(true)) if err != nil { cli.StopForError(err) @@ -265,13 +265,13 @@ func (cli *grpcClient) SetOptionSync(req types.RequestSetOption) (*types.Respons return reqres.Response.GetSetOption(), cli.Error() } -func (cli *grpcClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) { - reqres := cli.DeliverTxAsync(tx) +func (cli *grpcClient) DeliverTxSync(params types.RequestDeliverTx) (*types.ResponseDeliverTx, error) { + reqres := cli.DeliverTxAsync(params) return reqres.Response.GetDeliverTx(), cli.Error() } -func (cli *grpcClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { - reqres := cli.CheckTxAsync(tx) +func (cli *grpcClient) CheckTxSync(params types.RequestCheckTx) (*types.ResponseCheckTx, error) { + reqres := cli.CheckTxAsync(params) return reqres.Response.GetCheckTx(), cli.Error() } diff --git a/abci/client/local_client.go b/abci/client/local_client.go index 4a3e6a5ee..bb009173a 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -81,24 +81,24 @@ func (app *localClient) SetOptionAsync(req types.RequestSetOption) *ReqRes { ) } -func (app *localClient) DeliverTxAsync(tx []byte) *ReqRes { +func (app *localClient) DeliverTxAsync(params types.RequestDeliverTx) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.DeliverTx(types.RequestDeliverTx{Tx: tx}) + res := app.Application.DeliverTx(params) return app.callback( - types.ToRequestDeliverTx(tx), + types.ToRequestDeliverTx(params), types.ToResponseDeliverTx(res), ) } -func (app *localClient) CheckTxAsync(tx []byte) *ReqRes { +func (app *localClient) CheckTxAsync(req types.RequestCheckTx) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.CheckTx(types.RequestCheckTx{Tx: tx}) + res := app.Application.CheckTx(req) return app.callback( - types.ToRequestCheckTx(tx), + types.ToRequestCheckTx(req), types.ToResponseCheckTx(res), ) } @@ -184,19 +184,19 @@ func (app *localClient) SetOptionSync(req types.RequestSetOption) (*types.Respon return &res, nil } -func (app *localClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) { +func (app *localClient) DeliverTxSync(req types.RequestDeliverTx) (*types.ResponseDeliverTx, error) { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.DeliverTx(types.RequestDeliverTx{Tx: tx}) + res := app.Application.DeliverTx(req) return &res, nil } -func (app *localClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { +func (app *localClient) CheckTxSync(req types.RequestCheckTx) (*types.ResponseCheckTx, error) { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.CheckTx(types.RequestCheckTx{Tx: tx}) + res := app.Application.CheckTx(req) return &res, nil } diff --git a/abci/client/socket_client.go b/abci/client/socket_client.go index 3b401bd3c..16e39bf35 100644 --- a/abci/client/socket_client.go +++ b/abci/client/socket_client.go @@ -243,12 +243,12 @@ func (cli *socketClient) SetOptionAsync(req types.RequestSetOption) *ReqRes { return cli.queueRequest(types.ToRequestSetOption(req)) } -func (cli *socketClient) DeliverTxAsync(tx []byte) *ReqRes { - return cli.queueRequest(types.ToRequestDeliverTx(tx)) +func (cli *socketClient) DeliverTxAsync(req types.RequestDeliverTx) *ReqRes { + return cli.queueRequest(types.ToRequestDeliverTx(req)) } -func (cli *socketClient) CheckTxAsync(tx []byte) *ReqRes { - return cli.queueRequest(types.ToRequestCheckTx(tx)) +func (cli *socketClient) CheckTxAsync(req types.RequestCheckTx) *ReqRes { + return cli.queueRequest(types.ToRequestCheckTx(req)) } func (cli *socketClient) QueryAsync(req types.RequestQuery) *ReqRes { @@ -300,14 +300,14 @@ func (cli *socketClient) SetOptionSync(req types.RequestSetOption) (*types.Respo return reqres.Response.GetSetOption(), cli.Error() } -func (cli *socketClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, error) { - reqres := cli.queueRequest(types.ToRequestDeliverTx(tx)) +func (cli *socketClient) DeliverTxSync(req types.RequestDeliverTx) (*types.ResponseDeliverTx, error) { + reqres := cli.queueRequest(types.ToRequestDeliverTx(req)) cli.FlushSync() return reqres.Response.GetDeliverTx(), cli.Error() } -func (cli *socketClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { - reqres := cli.queueRequest(types.ToRequestCheckTx(tx)) +func (cli *socketClient) CheckTxSync(req types.RequestCheckTx) (*types.ResponseCheckTx, error) { + reqres := cli.queueRequest(types.ToRequestCheckTx(req)) cli.FlushSync() return reqres.Response.GetCheckTx(), cli.Error() } diff --git a/abci/cmd/abci-cli/abci-cli.go b/abci/cmd/abci-cli/abci-cli.go index 7e55569cb..cd0a6fd1f 100644 --- a/abci/cmd/abci-cli/abci-cli.go +++ b/abci/cmd/abci-cli/abci-cli.go @@ -546,7 +546,7 @@ func cmdDeliverTx(cmd *cobra.Command, args []string) error { if err != nil { return err } - res, err := client.DeliverTxSync(txBytes) + res, err := client.DeliverTxSync(types.RequestDeliverTx{Tx: txBytes}) if err != nil { return err } @@ -572,7 +572,7 @@ func cmdCheckTx(cmd *cobra.Command, args []string) error { if err != nil { return err } - res, err := client.CheckTxSync(txBytes) + res, err := client.CheckTxSync(types.RequestCheckTx{Tx: txBytes}) if err != nil { return err } diff --git a/abci/example/example_test.go b/abci/example/example_test.go index 677a2a481..6282f3a44 100644 --- a/abci/example/example_test.go +++ b/abci/example/example_test.go @@ -87,7 +87,7 @@ func testStream(t *testing.T, app types.Application) { // Write requests for counter := 0; counter < numDeliverTxs; counter++ { // Send request - reqRes := client.DeliverTxAsync([]byte("test")) + reqRes := client.DeliverTxAsync(types.RequestDeliverTx{Tx: []byte("test")}) _ = reqRes // check err ? diff --git a/abci/example/kvstore/kvstore_test.go b/abci/example/kvstore/kvstore_test.go index 074baa49f..1649d3e84 100644 --- a/abci/example/kvstore/kvstore_test.go +++ b/abci/example/kvstore/kvstore_test.go @@ -283,11 +283,11 @@ func runClientTests(t *testing.T, client abcicli.Client) { } func testClient(t *testing.T, app abcicli.Client, tx []byte, key, value string) { - ar, err := app.DeliverTxSync(tx) + ar, err := app.DeliverTxSync(types.RequestDeliverTx{Tx: tx}) require.NoError(t, err) require.False(t, ar.IsErr(), ar) // repeating tx doesn't raise error - ar, err = app.DeliverTxSync(tx) + ar, err = app.DeliverTxSync(types.RequestDeliverTx{Tx: tx}) require.NoError(t, err) require.False(t, ar.IsErr(), ar) diff --git a/abci/tests/server/client.go b/abci/tests/server/client.go index 5daa1e6af..58a413a4e 100644 --- a/abci/tests/server/client.go +++ b/abci/tests/server/client.go @@ -58,7 +58,7 @@ func Commit(client abcicli.Client, hashExp []byte) error { } func DeliverTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp []byte) error { - res, _ := client.DeliverTxSync(txBytes) + res, _ := client.DeliverTxSync(types.RequestDeliverTx{Tx: txBytes}) code, data, log := res.Code, res.Data, res.Log if code != codeExp { fmt.Println("Failed test: DeliverTx") @@ -77,7 +77,7 @@ func DeliverTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp [] } func CheckTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp []byte) error { - res, _ := client.CheckTxSync(txBytes) + res, _ := client.CheckTxSync(types.RequestCheckTx{Tx: txBytes}) code, data, log := res.Code, res.Data, res.Log if code != codeExp { fmt.Println("Failed test: CheckTx") diff --git a/abci/tests/test_app/app.go b/abci/tests/test_app/app.go index 25ed2f582..9c32fcc7d 100644 --- a/abci/tests/test_app/app.go +++ b/abci/tests/test_app/app.go @@ -43,7 +43,7 @@ func commit(client abcicli.Client, hashExp []byte) { } func deliverTx(client abcicli.Client, txBytes []byte, codeExp uint32, dataExp []byte) { - res, err := client.DeliverTxSync(txBytes) + res, err := client.DeliverTxSync(types.RequestDeliverTx{Tx: txBytes}) if err != nil { panicf("client error: %v", err) } diff --git a/abci/types/messages.go b/abci/types/messages.go index cb64a15d6..ad18727a8 100644 --- a/abci/types/messages.go +++ b/abci/types/messages.go @@ -93,15 +93,15 @@ func ToRequestSetOption(req RequestSetOption) *Request { } } -func ToRequestDeliverTx(tx []byte) *Request { +func ToRequestDeliverTx(req RequestDeliverTx) *Request { return &Request{ - Value: &Request_DeliverTx{&RequestDeliverTx{Tx: tx}}, + Value: &Request_DeliverTx{&req}, } } -func ToRequestCheckTx(tx []byte) *Request { +func ToRequestCheckTx(req RequestCheckTx) *Request { return &Request{ - Value: &Request_CheckTx{&RequestCheckTx{Tx: tx}}, + Value: &Request_CheckTx{&req}, } } diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index a7455b523..926d528ad 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -38,6 +38,29 @@ var _ = time.Kitchen // proto package needs to be updated. const _ = proto.GoGoProtoPackageIsVersion2 // please upgrade the proto package +type CheckTxType int32 + +const ( + CheckTxType_New CheckTxType = 0 + CheckTxType_Recheck CheckTxType = 1 +) + +var CheckTxType_name = map[int32]string{ + 0: "New", + 1: "Recheck", +} +var CheckTxType_value = map[string]int32{ + "New": 0, + "Recheck": 1, +} + +func (x CheckTxType) String() string { + return proto.EnumName(CheckTxType_name, int32(x)) +} +func (CheckTxType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor_types_30d8160a6576aafe, []int{0} +} + type Request struct { // Types that are valid to be assigned to Value: // *Request_Echo @@ -61,7 +84,7 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{0} + return fileDescriptor_types_30d8160a6576aafe, []int{0} } func (m *Request) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -483,7 +506,7 @@ func (m *RequestEcho) Reset() { *m = RequestEcho{} } func (m *RequestEcho) String() string { return proto.CompactTextString(m) } func (*RequestEcho) ProtoMessage() {} func (*RequestEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{1} + return fileDescriptor_types_30d8160a6576aafe, []int{1} } func (m *RequestEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -529,7 +552,7 @@ func (m *RequestFlush) Reset() { *m = RequestFlush{} } func (m *RequestFlush) String() string { return proto.CompactTextString(m) } func (*RequestFlush) ProtoMessage() {} func (*RequestFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{2} + return fileDescriptor_types_30d8160a6576aafe, []int{2} } func (m *RequestFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -571,7 +594,7 @@ func (m *RequestInfo) Reset() { *m = RequestInfo{} } func (m *RequestInfo) String() string { return proto.CompactTextString(m) } func (*RequestInfo) ProtoMessage() {} func (*RequestInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{3} + return fileDescriptor_types_30d8160a6576aafe, []int{3} } func (m *RequestInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -634,7 +657,7 @@ func (m *RequestSetOption) Reset() { *m = RequestSetOption{} } func (m *RequestSetOption) String() string { return proto.CompactTextString(m) } func (*RequestSetOption) ProtoMessage() {} func (*RequestSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{4} + return fileDescriptor_types_30d8160a6576aafe, []int{4} } func (m *RequestSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -692,7 +715,7 @@ func (m *RequestInitChain) Reset() { *m = RequestInitChain{} } func (m *RequestInitChain) String() string { return proto.CompactTextString(m) } func (*RequestInitChain) ProtoMessage() {} func (*RequestInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{5} + return fileDescriptor_types_30d8160a6576aafe, []int{5} } func (m *RequestInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -770,7 +793,7 @@ func (m *RequestQuery) Reset() { *m = RequestQuery{} } func (m *RequestQuery) String() string { return proto.CompactTextString(m) } func (*RequestQuery) ProtoMessage() {} func (*RequestQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{6} + return fileDescriptor_types_30d8160a6576aafe, []int{6} } func (m *RequestQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -841,7 +864,7 @@ func (m *RequestBeginBlock) Reset() { *m = RequestBeginBlock{} } func (m *RequestBeginBlock) String() string { return proto.CompactTextString(m) } func (*RequestBeginBlock) ProtoMessage() {} func (*RequestBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{7} + return fileDescriptor_types_30d8160a6576aafe, []int{7} } func (m *RequestBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -899,17 +922,18 @@ func (m *RequestBeginBlock) GetByzantineValidators() []Evidence { } type RequestCheckTx struct { - Tx []byte `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` - XXX_NoUnkeyedLiteral struct{} `json:"-"` - XXX_unrecognized []byte `json:"-"` - XXX_sizecache int32 `json:"-"` + Tx []byte `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` + Type CheckTxType `protobuf:"varint,2,opt,name=type,proto3,enum=types.CheckTxType" json:"type,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` } func (m *RequestCheckTx) Reset() { *m = RequestCheckTx{} } func (m *RequestCheckTx) String() string { return proto.CompactTextString(m) } func (*RequestCheckTx) ProtoMessage() {} func (*RequestCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{8} + return fileDescriptor_types_30d8160a6576aafe, []int{8} } func (m *RequestCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -945,6 +969,13 @@ func (m *RequestCheckTx) GetTx() []byte { return nil } +func (m *RequestCheckTx) GetType() CheckTxType { + if m != nil { + return m.Type + } + return CheckTxType_New +} + type RequestDeliverTx struct { Tx []byte `protobuf:"bytes,1,opt,name=tx,proto3" json:"tx,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` @@ -956,7 +987,7 @@ func (m *RequestDeliverTx) Reset() { *m = RequestDeliverTx{} } func (m *RequestDeliverTx) String() string { return proto.CompactTextString(m) } func (*RequestDeliverTx) ProtoMessage() {} func (*RequestDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{9} + return fileDescriptor_types_30d8160a6576aafe, []int{9} } func (m *RequestDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1003,7 +1034,7 @@ func (m *RequestEndBlock) Reset() { *m = RequestEndBlock{} } func (m *RequestEndBlock) String() string { return proto.CompactTextString(m) } func (*RequestEndBlock) ProtoMessage() {} func (*RequestEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{10} + return fileDescriptor_types_30d8160a6576aafe, []int{10} } func (m *RequestEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1049,7 +1080,7 @@ func (m *RequestCommit) Reset() { *m = RequestCommit{} } func (m *RequestCommit) String() string { return proto.CompactTextString(m) } func (*RequestCommit) ProtoMessage() {} func (*RequestCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{11} + return fileDescriptor_types_30d8160a6576aafe, []int{11} } func (m *RequestCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1102,7 +1133,7 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{12} + return fileDescriptor_types_30d8160a6576aafe, []int{12} } func (m *Response) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1555,7 +1586,7 @@ func (m *ResponseException) Reset() { *m = ResponseException{} } func (m *ResponseException) String() string { return proto.CompactTextString(m) } func (*ResponseException) ProtoMessage() {} func (*ResponseException) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{13} + return fileDescriptor_types_30d8160a6576aafe, []int{13} } func (m *ResponseException) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1602,7 +1633,7 @@ func (m *ResponseEcho) Reset() { *m = ResponseEcho{} } func (m *ResponseEcho) String() string { return proto.CompactTextString(m) } func (*ResponseEcho) ProtoMessage() {} func (*ResponseEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{14} + return fileDescriptor_types_30d8160a6576aafe, []int{14} } func (m *ResponseEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1648,7 +1679,7 @@ func (m *ResponseFlush) Reset() { *m = ResponseFlush{} } func (m *ResponseFlush) String() string { return proto.CompactTextString(m) } func (*ResponseFlush) ProtoMessage() {} func (*ResponseFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{15} + return fileDescriptor_types_30d8160a6576aafe, []int{15} } func (m *ResponseFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1692,7 +1723,7 @@ func (m *ResponseInfo) Reset() { *m = ResponseInfo{} } func (m *ResponseInfo) String() string { return proto.CompactTextString(m) } func (*ResponseInfo) ProtoMessage() {} func (*ResponseInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{16} + return fileDescriptor_types_30d8160a6576aafe, []int{16} } func (m *ResponseInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1771,7 +1802,7 @@ func (m *ResponseSetOption) Reset() { *m = ResponseSetOption{} } func (m *ResponseSetOption) String() string { return proto.CompactTextString(m) } func (*ResponseSetOption) ProtoMessage() {} func (*ResponseSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{17} + return fileDescriptor_types_30d8160a6576aafe, []int{17} } func (m *ResponseSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1833,7 +1864,7 @@ func (m *ResponseInitChain) Reset() { *m = ResponseInitChain{} } func (m *ResponseInitChain) String() string { return proto.CompactTextString(m) } func (*ResponseInitChain) ProtoMessage() {} func (*ResponseInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{18} + return fileDescriptor_types_30d8160a6576aafe, []int{18} } func (m *ResponseInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1896,7 +1927,7 @@ func (m *ResponseQuery) Reset() { *m = ResponseQuery{} } func (m *ResponseQuery) String() string { return proto.CompactTextString(m) } func (*ResponseQuery) ProtoMessage() {} func (*ResponseQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{19} + return fileDescriptor_types_30d8160a6576aafe, []int{19} } func (m *ResponseQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1999,7 +2030,7 @@ func (m *ResponseBeginBlock) Reset() { *m = ResponseBeginBlock{} } func (m *ResponseBeginBlock) String() string { return proto.CompactTextString(m) } func (*ResponseBeginBlock) ProtoMessage() {} func (*ResponseBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{20} + return fileDescriptor_types_30d8160a6576aafe, []int{20} } func (m *ResponseBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2053,7 +2084,7 @@ func (m *ResponseCheckTx) Reset() { *m = ResponseCheckTx{} } func (m *ResponseCheckTx) String() string { return proto.CompactTextString(m) } func (*ResponseCheckTx) ProtoMessage() {} func (*ResponseCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{21} + return fileDescriptor_types_30d8160a6576aafe, []int{21} } func (m *ResponseCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2156,7 +2187,7 @@ func (m *ResponseDeliverTx) Reset() { *m = ResponseDeliverTx{} } func (m *ResponseDeliverTx) String() string { return proto.CompactTextString(m) } func (*ResponseDeliverTx) ProtoMessage() {} func (*ResponseDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{22} + return fileDescriptor_types_30d8160a6576aafe, []int{22} } func (m *ResponseDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2254,7 +2285,7 @@ func (m *ResponseEndBlock) Reset() { *m = ResponseEndBlock{} } func (m *ResponseEndBlock) String() string { return proto.CompactTextString(m) } func (*ResponseEndBlock) ProtoMessage() {} func (*ResponseEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{23} + return fileDescriptor_types_30d8160a6576aafe, []int{23} } func (m *ResponseEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2316,7 +2347,7 @@ func (m *ResponseCommit) Reset() { *m = ResponseCommit{} } func (m *ResponseCommit) String() string { return proto.CompactTextString(m) } func (*ResponseCommit) ProtoMessage() {} func (*ResponseCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{24} + return fileDescriptor_types_30d8160a6576aafe, []int{24} } func (m *ResponseCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2367,7 +2398,7 @@ func (m *ConsensusParams) Reset() { *m = ConsensusParams{} } func (m *ConsensusParams) String() string { return proto.CompactTextString(m) } func (*ConsensusParams) ProtoMessage() {} func (*ConsensusParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{25} + return fileDescriptor_types_30d8160a6576aafe, []int{25} } func (m *ConsensusParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2417,7 +2448,7 @@ func (m *ConsensusParams) GetValidator() *ValidatorParams { return nil } -// BlockParams contains limits on the block size and timestamp. +// BlockParams contains limits on the block size. type BlockParams struct { // Note: must be greater than 0 MaxBytes int64 `protobuf:"varint,1,opt,name=max_bytes,json=maxBytes,proto3" json:"max_bytes,omitempty"` @@ -2432,7 +2463,7 @@ func (m *BlockParams) Reset() { *m = BlockParams{} } func (m *BlockParams) String() string { return proto.CompactTextString(m) } func (*BlockParams) ProtoMessage() {} func (*BlockParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{26} + return fileDescriptor_types_30d8160a6576aafe, []int{26} } func (m *BlockParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2488,7 +2519,7 @@ func (m *EvidenceParams) Reset() { *m = EvidenceParams{} } func (m *EvidenceParams) String() string { return proto.CompactTextString(m) } func (*EvidenceParams) ProtoMessage() {} func (*EvidenceParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{27} + return fileDescriptor_types_30d8160a6576aafe, []int{27} } func (m *EvidenceParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2536,7 +2567,7 @@ func (m *ValidatorParams) Reset() { *m = ValidatorParams{} } func (m *ValidatorParams) String() string { return proto.CompactTextString(m) } func (*ValidatorParams) ProtoMessage() {} func (*ValidatorParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{28} + return fileDescriptor_types_30d8160a6576aafe, []int{28} } func (m *ValidatorParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2584,7 +2615,7 @@ func (m *LastCommitInfo) Reset() { *m = LastCommitInfo{} } func (m *LastCommitInfo) String() string { return proto.CompactTextString(m) } func (*LastCommitInfo) ProtoMessage() {} func (*LastCommitInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{29} + return fileDescriptor_types_30d8160a6576aafe, []int{29} } func (m *LastCommitInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2639,7 +2670,7 @@ func (m *Event) Reset() { *m = Event{} } func (m *Event) String() string { return proto.CompactTextString(m) } func (*Event) ProtoMessage() {} func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{30} + return fileDescriptor_types_30d8160a6576aafe, []int{30} } func (m *Event) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2713,7 +2744,7 @@ func (m *Header) Reset() { *m = Header{} } func (m *Header) String() string { return proto.CompactTextString(m) } func (*Header) ProtoMessage() {} func (*Header) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{31} + return fileDescriptor_types_30d8160a6576aafe, []int{31} } func (m *Header) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2866,7 +2897,7 @@ func (m *Version) Reset() { *m = Version{} } func (m *Version) String() string { return proto.CompactTextString(m) } func (*Version) ProtoMessage() {} func (*Version) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{32} + return fileDescriptor_types_30d8160a6576aafe, []int{32} } func (m *Version) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2921,7 +2952,7 @@ func (m *BlockID) Reset() { *m = BlockID{} } func (m *BlockID) String() string { return proto.CompactTextString(m) } func (*BlockID) ProtoMessage() {} func (*BlockID) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{33} + return fileDescriptor_types_30d8160a6576aafe, []int{33} } func (m *BlockID) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2976,7 +3007,7 @@ func (m *PartSetHeader) Reset() { *m = PartSetHeader{} } func (m *PartSetHeader) String() string { return proto.CompactTextString(m) } func (*PartSetHeader) ProtoMessage() {} func (*PartSetHeader) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{34} + return fileDescriptor_types_30d8160a6576aafe, []int{34} } func (m *PartSetHeader) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3033,7 +3064,7 @@ func (m *Validator) Reset() { *m = Validator{} } func (m *Validator) String() string { return proto.CompactTextString(m) } func (*Validator) ProtoMessage() {} func (*Validator) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{35} + return fileDescriptor_types_30d8160a6576aafe, []int{35} } func (m *Validator) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3089,7 +3120,7 @@ func (m *ValidatorUpdate) Reset() { *m = ValidatorUpdate{} } func (m *ValidatorUpdate) String() string { return proto.CompactTextString(m) } func (*ValidatorUpdate) ProtoMessage() {} func (*ValidatorUpdate) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{36} + return fileDescriptor_types_30d8160a6576aafe, []int{36} } func (m *ValidatorUpdate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3145,7 +3176,7 @@ func (m *VoteInfo) Reset() { *m = VoteInfo{} } func (m *VoteInfo) String() string { return proto.CompactTextString(m) } func (*VoteInfo) ProtoMessage() {} func (*VoteInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{37} + return fileDescriptor_types_30d8160a6576aafe, []int{37} } func (m *VoteInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3200,7 +3231,7 @@ func (m *PubKey) Reset() { *m = PubKey{} } func (m *PubKey) String() string { return proto.CompactTextString(m) } func (*PubKey) ProtoMessage() {} func (*PubKey) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{38} + return fileDescriptor_types_30d8160a6576aafe, []int{38} } func (m *PubKey) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3258,7 +3289,7 @@ func (m *Evidence) Reset() { *m = Evidence{} } func (m *Evidence) String() string { return proto.CompactTextString(m) } func (*Evidence) ProtoMessage() {} func (*Evidence) Descriptor() ([]byte, []int) { - return fileDescriptor_types_62f0c59aeb977f78, []int{39} + return fileDescriptor_types_30d8160a6576aafe, []int{39} } func (m *Evidence) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3403,6 +3434,8 @@ func init() { golang_proto.RegisterType((*PubKey)(nil), "types.PubKey") proto.RegisterType((*Evidence)(nil), "types.Evidence") golang_proto.RegisterType((*Evidence)(nil), "types.Evidence") + proto.RegisterEnum("types.CheckTxType", CheckTxType_name, CheckTxType_value) + golang_proto.RegisterEnum("types.CheckTxType", CheckTxType_name, CheckTxType_value) } func (this *Request) Equal(that interface{}) bool { if that == nil { @@ -3958,6 +3991,9 @@ func (this *RequestCheckTx) Equal(that interface{}) bool { if !bytes.Equal(this.Tx, that1.Tx) { return false } + if this.Type != that1.Type { + return false + } if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { return false } @@ -6211,6 +6247,11 @@ func (m *RequestCheckTx) MarshalTo(dAtA []byte) (int, error) { i = encodeVarintTypes(dAtA, i, uint64(len(m.Tx))) i += copy(dAtA[i:], m.Tx) } + if m.Type != 0 { + dAtA[i] = 0x10 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.Type)) + } if m.XXX_unrecognized != nil { i += copy(dAtA[i:], m.XXX_unrecognized) } @@ -7872,8 +7913,9 @@ func NewPopulatedRequestCheckTx(r randyTypes, easy bool) *RequestCheckTx { for i := 0; i < v11; i++ { this.Tx[i] = byte(r.Intn(256)) } + this.Type = CheckTxType([]int32{0, 1}[r.Intn(2)]) if !easy && r.Intn(10) != 0 { - this.XXX_unrecognized = randUnrecognizedTypes(r, 2) + this.XXX_unrecognized = randUnrecognizedTypes(r, 3) } return this } @@ -8906,6 +8948,9 @@ func (m *RequestCheckTx) Size() (n int) { if l > 0 { n += 1 + l + sovTypes(uint64(l)) } + if m.Type != 0 { + n += 1 + sovTypes(uint64(m.Type)) + } if m.XXX_unrecognized != nil { n += len(m.XXX_unrecognized) } @@ -11127,6 +11172,25 @@ func (m *RequestCheckTx) Unmarshal(dAtA []byte) error { m.Tx = []byte{} } iNdEx = postIndex + case 2: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Type", wireType) + } + m.Type = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Type |= (CheckTxType(b) & 0x7F) << shift + if b < 0x80 { + break + } + } default: iNdEx = preIndex skippy, err := skipTypes(dAtA[iNdEx:]) @@ -15638,152 +15702,154 @@ var ( ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") ) -func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_62f0c59aeb977f78) } +func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_30d8160a6576aafe) } func init() { - golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_62f0c59aeb977f78) -} - -var fileDescriptor_types_62f0c59aeb977f78 = []byte{ - // 2241 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x58, 0x4b, 0x73, 0x1c, 0x49, - 0xf1, 0x57, 0xcf, 0xbb, 0x73, 0x34, 0x0f, 0x97, 0x65, 0x7b, 0x3c, 0x7f, 0xff, 0x25, 0x47, 0x1b, - 0x76, 0x25, 0xd6, 0x3b, 0xda, 0xd5, 0x62, 0x42, 0xc6, 0xcb, 0x46, 0x68, 0x6c, 0x83, 0x14, 0x6b, - 0x40, 0xb4, 0x6d, 0x71, 0x21, 0xa2, 0xa3, 0x66, 0xba, 0x3c, 0xd3, 0xe1, 0x99, 0xee, 0xde, 0xee, - 0x9a, 0xd9, 0x11, 0x47, 0xce, 0x7b, 0xd8, 0x03, 0x1f, 0x81, 0x03, 0x1f, 0x61, 0x8f, 0x9c, 0x88, - 0x3d, 0x72, 0xe0, 0x6c, 0x40, 0x04, 0x17, 0x22, 0x38, 0x03, 0x37, 0xa2, 0xb2, 0xaa, 0x9f, 0xea, - 0x31, 0xbb, 0x86, 0x1b, 0x17, 0xa9, 0xab, 0xf2, 0x97, 0xf5, 0xc8, 0xc9, 0xcc, 0x5f, 0x66, 0xc1, - 0x75, 0x3a, 0x1a, 0x3b, 0xfb, 0xfc, 0xdc, 0x67, 0xa1, 0xfc, 0x3b, 0xf0, 0x03, 0x8f, 0x7b, 0xa4, - 0x8a, 0x83, 0xfe, 0xbb, 0x13, 0x87, 0x4f, 0x17, 0xa3, 0xc1, 0xd8, 0x9b, 0xef, 0x4f, 0xbc, 0x89, - 0xb7, 0x8f, 0xd2, 0xd1, 0xe2, 0x05, 0x8e, 0x70, 0x80, 0x5f, 0x52, 0xab, 0xff, 0x20, 0x05, 0xe7, - 0xcc, 0xb5, 0x59, 0x30, 0x77, 0x5c, 0x9e, 0xfe, 0x1c, 0x07, 0xe7, 0x3e, 0xf7, 0xf6, 0xe7, 0x2c, - 0x78, 0x39, 0x63, 0xea, 0x9f, 0x52, 0x3e, 0xfc, 0xb7, 0xca, 0x33, 0x67, 0x14, 0xee, 0x8f, 0xbd, - 0xf9, 0xdc, 0x73, 0xd3, 0x87, 0xed, 0xef, 0x4c, 0x3c, 0x6f, 0x32, 0x63, 0xc9, 0xe1, 0xb8, 0x33, - 0x67, 0x21, 0xa7, 0x73, 0x5f, 0x02, 0x8c, 0xdf, 0x56, 0xa0, 0x6e, 0xb2, 0x4f, 0x16, 0x2c, 0xe4, - 0x64, 0x17, 0x2a, 0x6c, 0x3c, 0xf5, 0x7a, 0xa5, 0xdb, 0xda, 0x6e, 0xf3, 0x80, 0x0c, 0xe4, 0x42, - 0x4a, 0xfa, 0x78, 0x3c, 0xf5, 0x8e, 0x37, 0x4c, 0x44, 0x90, 0x77, 0xa0, 0xfa, 0x62, 0xb6, 0x08, - 0xa7, 0xbd, 0x32, 0x42, 0xaf, 0x66, 0xa1, 0xdf, 0x17, 0xa2, 0xe3, 0x0d, 0x53, 0x62, 0xc4, 0xb2, - 0x8e, 0xfb, 0xc2, 0xeb, 0x55, 0x8a, 0x96, 0x3d, 0x71, 0x5f, 0xe0, 0xb2, 0x02, 0x41, 0x0e, 0x01, - 0x42, 0xc6, 0x2d, 0xcf, 0xe7, 0x8e, 0xe7, 0xf6, 0xaa, 0x88, 0xbf, 0x91, 0xc5, 0x3f, 0x65, 0xfc, - 0xc7, 0x28, 0x3e, 0xde, 0x30, 0xf5, 0x30, 0x1a, 0x08, 0x4d, 0xc7, 0x75, 0xb8, 0x35, 0x9e, 0x52, - 0xc7, 0xed, 0xd5, 0x8a, 0x34, 0x4f, 0x5c, 0x87, 0x3f, 0x14, 0x62, 0xa1, 0xe9, 0x44, 0x03, 0x71, - 0x95, 0x4f, 0x16, 0x2c, 0x38, 0xef, 0xd5, 0x8b, 0xae, 0xf2, 0x13, 0x21, 0x12, 0x57, 0x41, 0x0c, - 0x79, 0x00, 0xcd, 0x11, 0x9b, 0x38, 0xae, 0x35, 0x9a, 0x79, 0xe3, 0x97, 0xbd, 0x06, 0xaa, 0xf4, - 0xb2, 0x2a, 0x43, 0x01, 0x18, 0x0a, 0xf9, 0xf1, 0x86, 0x09, 0xa3, 0x78, 0x44, 0x0e, 0xa0, 0x31, - 0x9e, 0xb2, 0xf1, 0x4b, 0x8b, 0xaf, 0x7a, 0x3a, 0x6a, 0x5e, 0xcb, 0x6a, 0x3e, 0x14, 0xd2, 0x67, - 0xab, 0xe3, 0x0d, 0xb3, 0x3e, 0x96, 0x9f, 0xe4, 0x1e, 0xe8, 0xcc, 0xb5, 0xd5, 0x76, 0x4d, 0x54, - 0xba, 0x9e, 0xfb, 0x5d, 0x5c, 0x3b, 0xda, 0xac, 0xc1, 0xd4, 0x37, 0x19, 0x40, 0x4d, 0x38, 0x83, - 0xc3, 0x7b, 0x9b, 0xa8, 0xb3, 0x95, 0xdb, 0x08, 0x65, 0xc7, 0x1b, 0xa6, 0x42, 0x09, 0xf3, 0xd9, - 0x6c, 0xe6, 0x2c, 0x59, 0x20, 0x0e, 0x77, 0xb5, 0xc8, 0x7c, 0x8f, 0xa4, 0x1c, 0x8f, 0xa7, 0xdb, - 0xd1, 0x60, 0x58, 0x87, 0xea, 0x92, 0xce, 0x16, 0xcc, 0x78, 0x1b, 0x9a, 0x29, 0x4f, 0x21, 0x3d, - 0xa8, 0xcf, 0x59, 0x18, 0xd2, 0x09, 0xeb, 0x69, 0xb7, 0xb5, 0x5d, 0xdd, 0x8c, 0x86, 0x46, 0x1b, - 0x36, 0xd3, 0x7e, 0x62, 0xcc, 0x63, 0x45, 0xe1, 0x0b, 0x42, 0x71, 0xc9, 0x82, 0x50, 0x38, 0x80, - 0x52, 0x54, 0x43, 0x72, 0x07, 0x5a, 0x68, 0x07, 0x2b, 0x92, 0x0b, 0x3f, 0xad, 0x98, 0x9b, 0x38, - 0x79, 0xa6, 0x40, 0x3b, 0xd0, 0xf4, 0x0f, 0xfc, 0x18, 0x52, 0x46, 0x08, 0xf8, 0x07, 0xbe, 0x02, - 0x18, 0xdf, 0x85, 0x6e, 0xde, 0x95, 0x48, 0x17, 0xca, 0x2f, 0xd9, 0xb9, 0xda, 0x4f, 0x7c, 0x92, - 0x2d, 0x75, 0x2d, 0xdc, 0x43, 0x37, 0xd5, 0x1d, 0x3f, 0x2f, 0xc5, 0xca, 0xb1, 0x37, 0x91, 0x43, - 0xa8, 0x88, 0xa0, 0x42, 0xed, 0xe6, 0x41, 0x7f, 0x20, 0x23, 0x6e, 0x10, 0x45, 0xdc, 0xe0, 0x59, - 0x14, 0x71, 0xc3, 0xc6, 0x97, 0xaf, 0x76, 0x36, 0x3e, 0xff, 0xc3, 0x8e, 0x66, 0xa2, 0x06, 0xb9, - 0x29, 0x1c, 0x82, 0x3a, 0xae, 0xe5, 0xd8, 0x6a, 0x9f, 0x3a, 0x8e, 0x4f, 0x6c, 0x72, 0x04, 0xdd, - 0xb1, 0xe7, 0x86, 0xcc, 0x0d, 0x17, 0xa1, 0xe5, 0xd3, 0x80, 0xce, 0x43, 0x15, 0x6b, 0xd1, 0xcf, - 0xff, 0x30, 0x12, 0x9f, 0xa2, 0xd4, 0xec, 0x8c, 0xb3, 0x13, 0xe4, 0x43, 0x80, 0x25, 0x9d, 0x39, - 0x36, 0xe5, 0x5e, 0x10, 0xf6, 0x2a, 0xb7, 0xcb, 0x29, 0xe5, 0xb3, 0x48, 0xf0, 0xdc, 0xb7, 0x29, - 0x67, 0xc3, 0x8a, 0x38, 0x99, 0x99, 0xc2, 0x93, 0xb7, 0xa0, 0x43, 0x7d, 0xdf, 0x0a, 0x39, 0xe5, - 0xcc, 0x1a, 0x9d, 0x73, 0x16, 0x62, 0x3c, 0x6e, 0x9a, 0x2d, 0xea, 0xfb, 0x4f, 0xc5, 0xec, 0x50, - 0x4c, 0x1a, 0x76, 0xfc, 0x6b, 0x62, 0xa8, 0x10, 0x02, 0x15, 0x9b, 0x72, 0x8a, 0xd6, 0xd8, 0x34, - 0xf1, 0x5b, 0xcc, 0xf9, 0x94, 0x4f, 0xd5, 0x1d, 0xf1, 0x9b, 0x5c, 0x87, 0xda, 0x94, 0x39, 0x93, - 0x29, 0xc7, 0x6b, 0x95, 0x4d, 0x35, 0x12, 0x86, 0xf7, 0x03, 0x6f, 0xc9, 0x30, 0x5b, 0x34, 0x4c, - 0x39, 0x30, 0xfe, 0xa2, 0xc1, 0x95, 0x4b, 0xe1, 0x25, 0xd6, 0x9d, 0xd2, 0x70, 0x1a, 0xed, 0x25, - 0xbe, 0xc9, 0x3b, 0x62, 0x5d, 0x6a, 0xb3, 0x40, 0x65, 0xb1, 0x96, 0xba, 0xf1, 0x31, 0x4e, 0xaa, - 0x8b, 0x2a, 0x08, 0x79, 0x0c, 0xdd, 0x19, 0x0d, 0xb9, 0x25, 0xa3, 0xc0, 0xc2, 0x2c, 0x55, 0xce, - 0x44, 0xe6, 0x13, 0x1a, 0x45, 0x8b, 0x70, 0x4e, 0xa5, 0xde, 0x9e, 0x65, 0x66, 0xc9, 0x31, 0x6c, - 0x8d, 0xce, 0x7f, 0x4e, 0x5d, 0xee, 0xb8, 0xcc, 0xba, 0x64, 0xf3, 0x8e, 0x5a, 0xea, 0xf1, 0xd2, - 0xb1, 0x99, 0x3b, 0x8e, 0x8c, 0x7d, 0x35, 0x56, 0x89, 0x7f, 0x8c, 0xd0, 0xb8, 0x0d, 0xed, 0x6c, - 0x2e, 0x20, 0x6d, 0x28, 0xf1, 0x95, 0xba, 0x61, 0x89, 0xaf, 0x0c, 0x23, 0xf6, 0xc0, 0x38, 0x20, - 0x2f, 0x61, 0xf6, 0xa0, 0x93, 0x4b, 0x0e, 0x29, 0x73, 0x6b, 0x69, 0x73, 0x1b, 0x1d, 0x68, 0x65, - 0x72, 0x82, 0xf1, 0x59, 0x15, 0x1a, 0x26, 0x0b, 0x7d, 0xe1, 0x4c, 0xe4, 0x10, 0x74, 0xb6, 0x1a, - 0x33, 0x99, 0x8e, 0xb5, 0x5c, 0xb2, 0x93, 0x98, 0xc7, 0x91, 0x5c, 0xa4, 0x85, 0x18, 0x4c, 0xf6, - 0x32, 0x54, 0x72, 0x35, 0xaf, 0x94, 0xe6, 0x92, 0xbb, 0x59, 0x2e, 0xd9, 0xca, 0x61, 0x73, 0x64, - 0xb2, 0x97, 0x21, 0x93, 0xfc, 0xc2, 0x19, 0x36, 0xb9, 0x5f, 0xc0, 0x26, 0xf9, 0xe3, 0xaf, 0xa1, - 0x93, 0xfb, 0x05, 0x74, 0xd2, 0xbb, 0xb4, 0x57, 0x21, 0x9f, 0xdc, 0xcd, 0xf2, 0x49, 0xfe, 0x3a, - 0x39, 0x42, 0xf9, 0xb0, 0x88, 0x50, 0x6e, 0xe6, 0x74, 0xd6, 0x32, 0xca, 0x07, 0x97, 0x18, 0xe5, - 0x7a, 0x4e, 0xb5, 0x80, 0x52, 0xee, 0x67, 0x72, 0x3d, 0x14, 0xde, 0xad, 0x38, 0xd9, 0x93, 0xef, - 0x5c, 0x66, 0xa3, 0x1b, 0xf9, 0x9f, 0xb6, 0x88, 0x8e, 0xf6, 0x73, 0x74, 0x74, 0x2d, 0x7f, 0xca, - 0x1c, 0x1f, 0x25, 0xac, 0xb2, 0x27, 0xe2, 0x3e, 0xe7, 0x69, 0x22, 0x47, 0xb0, 0x20, 0xf0, 0x02, - 0x95, 0xb0, 0xe5, 0xc0, 0xd8, 0x15, 0x99, 0x28, 0xf1, 0xaf, 0xd7, 0x30, 0x10, 0x3a, 0x7d, 0xca, - 0xbb, 0x8c, 0x2f, 0xb4, 0x44, 0x17, 0x23, 0x3a, 0x9d, 0xc5, 0x74, 0x95, 0xc5, 0x52, 0xc4, 0x54, - 0xca, 0x12, 0xd3, 0x0e, 0x34, 0x45, 0xae, 0xcc, 0x71, 0x0e, 0xf5, 0x23, 0xce, 0x21, 0xdf, 0x82, - 0x2b, 0x98, 0x67, 0x24, 0x7d, 0xa9, 0x40, 0xac, 0x60, 0x20, 0x76, 0x84, 0x40, 0x5a, 0x4c, 0x26, - 0xc0, 0x77, 0xe1, 0x6a, 0x0a, 0x2b, 0xd6, 0xc5, 0x1c, 0x27, 0x93, 0x6f, 0x37, 0x46, 0x1f, 0xf9, - 0xfe, 0x31, 0x0d, 0xa7, 0xc6, 0x0f, 0x13, 0x03, 0x25, 0x7c, 0x46, 0xa0, 0x32, 0xf6, 0x6c, 0x79, - 0xef, 0x96, 0x89, 0xdf, 0x82, 0xe3, 0x66, 0xde, 0x04, 0x0f, 0xa7, 0x9b, 0xe2, 0x53, 0xa0, 0xe2, - 0x50, 0xd2, 0x65, 0xcc, 0x18, 0xbf, 0xd4, 0x92, 0xf5, 0x12, 0x8a, 0x2b, 0x62, 0x23, 0xed, 0x3f, - 0x61, 0xa3, 0xd2, 0xd7, 0x63, 0x23, 0xe3, 0x42, 0x4b, 0x7e, 0xb2, 0x98, 0x67, 0xde, 0xec, 0x8a, - 0xc2, 0x7b, 0x1c, 0xd7, 0x66, 0x2b, 0x34, 0x69, 0xd9, 0x94, 0x83, 0xa8, 0x04, 0xa8, 0xa1, 0x99, - 0xb3, 0x25, 0x40, 0x1d, 0xe7, 0xe4, 0x80, 0xdc, 0x41, 0x7e, 0xf2, 0x5e, 0xa8, 0x50, 0x6d, 0x0d, - 0x54, 0xa1, 0x7e, 0x2a, 0x26, 0x4d, 0x29, 0x4b, 0x65, 0x5b, 0x3d, 0x43, 0x6e, 0xb7, 0x40, 0x17, - 0x07, 0x0d, 0x7d, 0x3a, 0x66, 0x18, 0x79, 0xba, 0x99, 0x4c, 0x18, 0xcf, 0x80, 0x5c, 0x8e, 0x78, - 0xf2, 0x11, 0xd4, 0xd8, 0x92, 0xb9, 0x5c, 0x58, 0x5c, 0x18, 0x6d, 0x33, 0xa6, 0x13, 0xe6, 0xf2, - 0x61, 0x4f, 0x98, 0xea, 0xaf, 0xaf, 0x76, 0xba, 0x12, 0x73, 0xd7, 0x9b, 0x3b, 0x9c, 0xcd, 0x7d, - 0x7e, 0x6e, 0x2a, 0x2d, 0xe3, 0xef, 0x9a, 0x60, 0x83, 0x4c, 0x36, 0x28, 0x34, 0x5e, 0xe4, 0xf2, - 0xa5, 0x14, 0x71, 0x7f, 0x35, 0x83, 0xfe, 0x3f, 0xc0, 0x84, 0x86, 0xd6, 0xa7, 0xd4, 0xe5, 0xcc, - 0x56, 0x56, 0xd5, 0x27, 0x34, 0xfc, 0x29, 0x4e, 0x88, 0x2a, 0x47, 0x88, 0x17, 0x21, 0xb3, 0xd1, - 0xbc, 0x65, 0xb3, 0x3e, 0xa1, 0xe1, 0xf3, 0x90, 0xd9, 0xa9, 0xbb, 0xd5, 0xdf, 0xe4, 0x6e, 0x59, - 0x7b, 0x36, 0xf2, 0xf6, 0xfc, 0x67, 0xca, 0x97, 0x13, 0xb2, 0xfc, 0xdf, 0xb8, 0xfb, 0xdf, 0x34, - 0x51, 0x27, 0x64, 0x53, 0x32, 0x39, 0x81, 0x2b, 0x71, 0x4c, 0x59, 0x0b, 0x8c, 0xb5, 0xc8, 0xab, - 0x5e, 0x1f, 0x8a, 0xdd, 0x65, 0x76, 0x3a, 0x24, 0x3f, 0x82, 0x1b, 0xb9, 0x8c, 0x10, 0x2f, 0x58, - 0x7a, 0x6d, 0x62, 0xb8, 0x96, 0x4d, 0x0c, 0xd1, 0x7a, 0x89, 0x35, 0xca, 0x6f, 0xe4, 0xe5, 0xdf, - 0x10, 0x85, 0x53, 0x9a, 0x4c, 0x8a, 0x7e, 0x53, 0xe3, 0x57, 0x1a, 0x74, 0x72, 0x07, 0x22, 0xbb, - 0x50, 0x95, 0x7c, 0xa6, 0x65, 0xda, 0x53, 0xb4, 0x98, 0x3a, 0xb3, 0x04, 0x90, 0xf7, 0xa1, 0xc1, - 0x54, 0x0d, 0xa7, 0x2e, 0x79, 0x2d, 0x57, 0xda, 0x29, 0x7c, 0x0c, 0x23, 0xdf, 0x06, 0x3d, 0x36, - 0x5d, 0xae, 0x7e, 0x8f, 0x2d, 0xad, 0x94, 0x12, 0xa0, 0xf1, 0x10, 0x9a, 0xa9, 0xed, 0xc9, 0xff, - 0x81, 0x3e, 0xa7, 0x2b, 0x55, 0x84, 0xcb, 0xf2, 0xad, 0x31, 0xa7, 0x2b, 0xac, 0xbf, 0xc9, 0x0d, - 0xa8, 0x0b, 0xe1, 0x84, 0x4a, 0xc3, 0x97, 0xcd, 0xda, 0x9c, 0xae, 0x7e, 0x40, 0x43, 0x63, 0x0f, - 0xda, 0xd9, 0x63, 0x45, 0xd0, 0x88, 0x10, 0x25, 0xf4, 0x68, 0xc2, 0x8c, 0x7b, 0xd0, 0xc9, 0x9d, - 0x86, 0x18, 0xd0, 0xf2, 0x17, 0x23, 0xeb, 0x25, 0x3b, 0xb7, 0xf0, 0xb8, 0xe8, 0x26, 0xba, 0xd9, - 0xf4, 0x17, 0xa3, 0x8f, 0xd9, 0xf9, 0x33, 0x31, 0x65, 0x3c, 0x85, 0x76, 0xb6, 0x3c, 0x16, 0x29, - 0x33, 0xf0, 0x16, 0xae, 0x8d, 0xeb, 0x57, 0x4d, 0x39, 0x10, 0x1d, 0xf6, 0xd2, 0x93, 0x9e, 0x91, - 0xae, 0x87, 0xcf, 0x3c, 0xce, 0x52, 0x45, 0xb5, 0xc4, 0x18, 0x0e, 0x54, 0xf1, 0x37, 0x17, 0xbf, - 0x9f, 0xc0, 0x45, 0x14, 0x2c, 0xbe, 0xc9, 0x13, 0x00, 0xca, 0x79, 0xe0, 0x8c, 0x16, 0xc9, 0x72, - 0xed, 0x81, 0x7c, 0xf6, 0x18, 0x7c, 0x7c, 0x76, 0x4a, 0x9d, 0x60, 0x78, 0x4b, 0xf9, 0xca, 0x56, - 0x82, 0x4c, 0xf9, 0x4b, 0x4a, 0xdf, 0xf8, 0x45, 0x15, 0x6a, 0xb2, 0x2d, 0x20, 0x83, 0x6c, 0xd3, - 0x29, 0x56, 0x55, 0x87, 0x94, 0xb3, 0xea, 0x8c, 0x31, 0xe3, 0xbf, 0x95, 0xef, 0xdc, 0x86, 0xcd, - 0x8b, 0x57, 0x3b, 0x75, 0x64, 0xcb, 0x93, 0x47, 0x49, 0x1b, 0xb7, 0xae, 0xcb, 0x89, 0x7a, 0xc6, - 0xca, 0xd7, 0xee, 0x19, 0x6f, 0x40, 0xdd, 0x5d, 0xcc, 0x2d, 0xbe, 0x0a, 0x55, 0xb6, 0xa9, 0xb9, - 0x8b, 0xf9, 0xb3, 0x15, 0x7a, 0x09, 0xf7, 0x38, 0x9d, 0xa1, 0x48, 0xe6, 0x9a, 0x06, 0x4e, 0x08, - 0xe1, 0x21, 0xb4, 0x52, 0x45, 0x85, 0x63, 0xab, 0xe2, 0xb4, 0x9d, 0x76, 0xf6, 0x93, 0x47, 0xea, - 0x96, 0xcd, 0xb8, 0xc8, 0x38, 0xb1, 0xc9, 0x6e, 0xb6, 0x45, 0xc2, 0x5a, 0xa4, 0x81, 0x21, 0x95, - 0xea, 0x82, 0x44, 0x25, 0x22, 0x0e, 0x20, 0x82, 0x4c, 0x42, 0x74, 0x84, 0x34, 0xc4, 0x04, 0x0a, - 0xdf, 0x86, 0x4e, 0x42, 0xe7, 0x12, 0x02, 0x72, 0x95, 0x64, 0x1a, 0x81, 0xef, 0xc1, 0x96, 0xcb, - 0x56, 0xdc, 0xca, 0xa3, 0x9b, 0x88, 0x26, 0x42, 0x76, 0x96, 0xd5, 0xf8, 0x26, 0xb4, 0x93, 0x54, - 0x84, 0xd8, 0x4d, 0xd9, 0xa8, 0xc6, 0xb3, 0x08, 0xbb, 0x09, 0x8d, 0xb8, 0x98, 0x6a, 0x21, 0xa0, - 0x4e, 0x65, 0x0d, 0x15, 0x97, 0x67, 0x01, 0x0b, 0x17, 0x33, 0xae, 0x16, 0x69, 0x23, 0x06, 0xcb, - 0x33, 0x53, 0xce, 0x23, 0xf6, 0x0e, 0xb4, 0xa2, 0xe8, 0x96, 0xb8, 0x0e, 0xe2, 0x36, 0xa3, 0x49, - 0x04, 0xed, 0x41, 0xd7, 0x0f, 0x3c, 0xdf, 0x0b, 0x59, 0x60, 0x51, 0xdb, 0x0e, 0x58, 0x18, 0xf6, - 0xba, 0x72, 0xbd, 0x68, 0xfe, 0x48, 0x4e, 0x1b, 0xef, 0x43, 0x3d, 0xaa, 0x12, 0xb7, 0xa0, 0x3a, - 0x8c, 0x33, 0x51, 0xc5, 0x94, 0x03, 0xc1, 0x43, 0x47, 0xbe, 0xaf, 0xde, 0x3a, 0xc4, 0xa7, 0xf1, - 0x33, 0xa8, 0xab, 0x1f, 0xac, 0xb0, 0x03, 0xfe, 0x1e, 0x6c, 0xfa, 0x34, 0x10, 0xd7, 0x48, 0xf7, - 0xc1, 0x51, 0x1f, 0x72, 0x4a, 0x03, 0xfe, 0x94, 0xf1, 0x4c, 0x3b, 0xdc, 0x44, 0xbc, 0x9c, 0x32, - 0xee, 0x43, 0x2b, 0x83, 0x11, 0xc7, 0x42, 0x3f, 0x8a, 0x82, 0x1a, 0x07, 0xf1, 0xce, 0xa5, 0x64, - 0x67, 0xe3, 0x01, 0xe8, 0xf1, 0x6f, 0x23, 0xca, 0xe5, 0xe8, 0xea, 0x9a, 0x32, 0xb7, 0x1c, 0x62, - 0x8b, 0xef, 0x7d, 0xca, 0x02, 0x15, 0x13, 0x72, 0x60, 0x3c, 0x4f, 0x25, 0x21, 0xc9, 0x0a, 0xe4, - 0x2e, 0xd4, 0x55, 0x12, 0x52, 0x51, 0x19, 0x35, 0xf3, 0xa7, 0x98, 0x85, 0xa2, 0x66, 0x5e, 0xe6, - 0xa4, 0x64, 0xd9, 0x52, 0x7a, 0xd9, 0x19, 0x34, 0xa2, 0x44, 0x93, 0xcd, 0xc6, 0x72, 0xc5, 0x6e, - 0x3e, 0x1b, 0xab, 0x45, 0x13, 0xa0, 0xf0, 0x8e, 0xd0, 0x99, 0xb8, 0xcc, 0xb6, 0x92, 0x10, 0xc2, - 0x3d, 0x1a, 0x66, 0x47, 0x0a, 0x9e, 0x44, 0xf1, 0x62, 0xbc, 0x07, 0x35, 0x79, 0xb6, 0xc2, 0xf4, - 0x55, 0x44, 0x49, 0xbf, 0xd7, 0xa0, 0x11, 0xe5, 0xe9, 0x42, 0xa5, 0xcc, 0xa1, 0x4b, 0x5f, 0xf5, - 0xd0, 0xff, 0xfd, 0xc4, 0x73, 0x17, 0x88, 0xcc, 0x2f, 0x4b, 0x8f, 0x3b, 0xee, 0xc4, 0x92, 0xb6, - 0x96, 0x39, 0xa8, 0x8b, 0x92, 0x33, 0x14, 0x9c, 0x8a, 0xf9, 0x83, 0xcf, 0xaa, 0xd0, 0x39, 0x1a, - 0x3e, 0x3c, 0x39, 0xf2, 0xfd, 0x99, 0x33, 0xa6, 0xd8, 0x95, 0xec, 0x43, 0x05, 0x1b, 0xb3, 0x82, - 0x87, 0xe5, 0x7e, 0xd1, 0x0b, 0x01, 0x39, 0x80, 0x2a, 0xf6, 0x67, 0xa4, 0xe8, 0x7d, 0xb9, 0x5f, - 0xf8, 0x50, 0x20, 0x36, 0x91, 0x1d, 0xdc, 0xe5, 0x67, 0xe6, 0x7e, 0xd1, 0x6b, 0x01, 0xf9, 0x08, - 0xf4, 0xa4, 0x71, 0x5a, 0xf7, 0xd8, 0xdc, 0x5f, 0xfb, 0x6e, 0x20, 0xf4, 0x93, 0xe2, 0x72, 0xdd, - 0x9b, 0x69, 0x7f, 0x6d, 0x83, 0x4d, 0x0e, 0xa1, 0x1e, 0x95, 0xe5, 0xc5, 0xcf, 0xc1, 0xfd, 0x35, - 0x3d, 0xbd, 0x30, 0x8f, 0xec, 0x85, 0x8a, 0xde, 0xac, 0xfb, 0x85, 0x0f, 0x0f, 0xe4, 0x1e, 0xd4, - 0x54, 0x7d, 0x54, 0xf8, 0x24, 0xdc, 0x2f, 0xee, 0xcc, 0xc5, 0x25, 0x93, 0x6e, 0x70, 0xdd, 0xbb, - 0x7a, 0x7f, 0xed, 0x0b, 0x09, 0x39, 0x02, 0x48, 0xb5, 0x34, 0x6b, 0x1f, 0xcc, 0xfb, 0xeb, 0x5f, - 0x3e, 0xc8, 0x03, 0x68, 0x24, 0xaf, 0x59, 0xc5, 0x4f, 0xe0, 0xfd, 0x75, 0x8f, 0x11, 0xc3, 0x5b, - 0xff, 0xf8, 0xd3, 0xb6, 0xf6, 0xeb, 0x8b, 0x6d, 0xed, 0x8b, 0x8b, 0x6d, 0xed, 0xcb, 0x8b, 0x6d, - 0xed, 0x77, 0x17, 0xdb, 0xda, 0x1f, 0x2f, 0xb6, 0xb5, 0xdf, 0xfc, 0x79, 0x5b, 0x1b, 0xd5, 0xd0, - 0xfd, 0x3f, 0xf8, 0x57, 0x00, 0x00, 0x00, 0xff, 0xff, 0x3e, 0x33, 0xc0, 0x3b, 0xf2, 0x19, 0x00, - 0x00, + golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_30d8160a6576aafe) +} + +var fileDescriptor_types_30d8160a6576aafe = []byte{ + // 2282 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x58, 0xcd, 0x73, 0x1c, 0x47, + 0x15, 0xd7, 0xec, 0xf7, 0xbc, 0xd5, 0x7e, 0xb8, 0x2d, 0xdb, 0xeb, 0xc5, 0x48, 0xae, 0x31, 0x38, + 0x52, 0xe2, 0xac, 0x12, 0x05, 0x53, 0x32, 0x0e, 0xa9, 0xd2, 0xda, 0x06, 0xa9, 0x62, 0x82, 0x18, + 0xdb, 0xe2, 0x42, 0xd5, 0x54, 0xef, 0x4e, 0x7b, 0x77, 0xca, 0xbb, 0x33, 0x93, 0x99, 0x5e, 0x79, + 0xc5, 0x91, 0x73, 0x0e, 0x39, 0xf0, 0x27, 0x70, 0xe0, 0x4f, 0xc8, 0x91, 0x13, 0x95, 0x23, 0x07, + 0xce, 0x06, 0x44, 0x71, 0xa1, 0x8a, 0x33, 0x70, 0xa3, 0xfa, 0x75, 0xcf, 0xa7, 0x66, 0x4d, 0x62, + 0xb8, 0xe5, 0xb2, 0x3b, 0xdd, 0xef, 0xf7, 0x7a, 0xba, 0xdf, 0xbc, 0xf7, 0x7e, 0xef, 0x35, 0x5c, + 0xa5, 0xa3, 0xb1, 0xb3, 0xcb, 0xcf, 0x7c, 0x16, 0xca, 0xdf, 0x81, 0x1f, 0x78, 0xdc, 0x23, 0x55, + 0x1c, 0xf4, 0xdf, 0x9d, 0x38, 0x7c, 0xba, 0x18, 0x0d, 0xc6, 0xde, 0x7c, 0x77, 0xe2, 0x4d, 0xbc, + 0x5d, 0x94, 0x8e, 0x16, 0xcf, 0x71, 0x84, 0x03, 0x7c, 0x92, 0x5a, 0xfd, 0xfb, 0x29, 0x38, 0x67, + 0xae, 0xcd, 0x82, 0xb9, 0xe3, 0xf2, 0xf4, 0xe3, 0x38, 0x38, 0xf3, 0xb9, 0xb7, 0x3b, 0x67, 0xc1, + 0x8b, 0x19, 0x53, 0x7f, 0x4a, 0x79, 0xff, 0xbf, 0x2a, 0xcf, 0x9c, 0x51, 0xb8, 0x3b, 0xf6, 0xe6, + 0x73, 0xcf, 0x4d, 0x6f, 0xb6, 0xbf, 0x35, 0xf1, 0xbc, 0xc9, 0x8c, 0x25, 0x9b, 0xe3, 0xce, 0x9c, + 0x85, 0x9c, 0xce, 0x7d, 0x09, 0x30, 0x7e, 0x5f, 0x81, 0xba, 0xc9, 0x3e, 0x5d, 0xb0, 0x90, 0x93, + 0x6d, 0xa8, 0xb0, 0xf1, 0xd4, 0xeb, 0x95, 0x6e, 0x6a, 0xdb, 0xcd, 0x3d, 0x32, 0x90, 0x0b, 0x29, + 0xe9, 0xa3, 0xf1, 0xd4, 0x3b, 0x5c, 0x33, 0x11, 0x41, 0xde, 0x81, 0xea, 0xf3, 0xd9, 0x22, 0x9c, + 0xf6, 0xca, 0x08, 0xbd, 0x9c, 0x85, 0xfe, 0x48, 0x88, 0x0e, 0xd7, 0x4c, 0x89, 0x11, 0xcb, 0x3a, + 0xee, 0x73, 0xaf, 0x57, 0x29, 0x5a, 0xf6, 0xc8, 0x7d, 0x8e, 0xcb, 0x0a, 0x04, 0xd9, 0x07, 0x08, + 0x19, 0xb7, 0x3c, 0x9f, 0x3b, 0x9e, 0xdb, 0xab, 0x22, 0xfe, 0x5a, 0x16, 0xff, 0x84, 0xf1, 0x9f, + 0xa2, 0xf8, 0x70, 0xcd, 0xd4, 0xc3, 0x68, 0x20, 0x34, 0x1d, 0xd7, 0xe1, 0xd6, 0x78, 0x4a, 0x1d, + 0xb7, 0x57, 0x2b, 0xd2, 0x3c, 0x72, 0x1d, 0xfe, 0x40, 0x88, 0x85, 0xa6, 0x13, 0x0d, 0xc4, 0x51, + 0x3e, 0x5d, 0xb0, 0xe0, 0xac, 0x57, 0x2f, 0x3a, 0xca, 0xcf, 0x84, 0x48, 0x1c, 0x05, 0x31, 0xe4, + 0x3e, 0x34, 0x47, 0x6c, 0xe2, 0xb8, 0xd6, 0x68, 0xe6, 0x8d, 0x5f, 0xf4, 0x1a, 0xa8, 0xd2, 0xcb, + 0xaa, 0x0c, 0x05, 0x60, 0x28, 0xe4, 0x87, 0x6b, 0x26, 0x8c, 0xe2, 0x11, 0xd9, 0x83, 0xc6, 0x78, + 0xca, 0xc6, 0x2f, 0x2c, 0xbe, 0xec, 0xe9, 0xa8, 0x79, 0x25, 0xab, 0xf9, 0x40, 0x48, 0x9f, 0x2e, + 0x0f, 0xd7, 0xcc, 0xfa, 0x58, 0x3e, 0x92, 0xbb, 0xa0, 0x33, 0xd7, 0x56, 0xaf, 0x6b, 0xa2, 0xd2, + 0xd5, 0xdc, 0x77, 0x71, 0xed, 0xe8, 0x65, 0x0d, 0xa6, 0x9e, 0xc9, 0x00, 0x6a, 0xc2, 0x19, 0x1c, + 0xde, 0x5b, 0x47, 0x9d, 0x8d, 0xdc, 0x8b, 0x50, 0x76, 0xb8, 0x66, 0x2a, 0x94, 0x30, 0x9f, 0xcd, + 0x66, 0xce, 0x29, 0x0b, 0xc4, 0xe6, 0x2e, 0x17, 0x99, 0xef, 0xa1, 0x94, 0xe3, 0xf6, 0x74, 0x3b, + 0x1a, 0x0c, 0xeb, 0x50, 0x3d, 0xa5, 0xb3, 0x05, 0x33, 0xde, 0x82, 0x66, 0xca, 0x53, 0x48, 0x0f, + 0xea, 0x73, 0x16, 0x86, 0x74, 0xc2, 0x7a, 0xda, 0x4d, 0x6d, 0x5b, 0x37, 0xa3, 0xa1, 0xd1, 0x86, + 0xf5, 0xb4, 0x9f, 0x18, 0xf3, 0x58, 0x51, 0xf8, 0x82, 0x50, 0x3c, 0x65, 0x41, 0x28, 0x1c, 0x40, + 0x29, 0xaa, 0x21, 0xb9, 0x05, 0x2d, 0xb4, 0x83, 0x15, 0xc9, 0x85, 0x9f, 0x56, 0xcc, 0x75, 0x9c, + 0x3c, 0x51, 0xa0, 0x2d, 0x68, 0xfa, 0x7b, 0x7e, 0x0c, 0x29, 0x23, 0x04, 0xfc, 0x3d, 0x5f, 0x01, + 0x8c, 0x1f, 0x40, 0x37, 0xef, 0x4a, 0xa4, 0x0b, 0xe5, 0x17, 0xec, 0x4c, 0xbd, 0x4f, 0x3c, 0x92, + 0x0d, 0x75, 0x2c, 0x7c, 0x87, 0x6e, 0xaa, 0x33, 0x7e, 0x5e, 0x8a, 0x95, 0x63, 0x6f, 0x22, 0xfb, + 0x50, 0x11, 0x41, 0x85, 0xda, 0xcd, 0xbd, 0xfe, 0x40, 0x46, 0xdc, 0x20, 0x8a, 0xb8, 0xc1, 0xd3, + 0x28, 0xe2, 0x86, 0x8d, 0x2f, 0x5f, 0x6d, 0xad, 0x7d, 0xfe, 0xa7, 0x2d, 0xcd, 0x44, 0x0d, 0x72, + 0x5d, 0x38, 0x04, 0x75, 0x5c, 0xcb, 0xb1, 0xd5, 0x7b, 0xea, 0x38, 0x3e, 0xb2, 0xc9, 0x01, 0x74, + 0xc7, 0x9e, 0x1b, 0x32, 0x37, 0x5c, 0x84, 0x96, 0x4f, 0x03, 0x3a, 0x0f, 0x55, 0xac, 0x45, 0x9f, + 0xff, 0x41, 0x24, 0x3e, 0x46, 0xa9, 0xd9, 0x19, 0x67, 0x27, 0xc8, 0x87, 0x00, 0xa7, 0x74, 0xe6, + 0xd8, 0x94, 0x7b, 0x41, 0xd8, 0xab, 0xdc, 0x2c, 0xa7, 0x94, 0x4f, 0x22, 0xc1, 0x33, 0xdf, 0xa6, + 0x9c, 0x0d, 0x2b, 0x62, 0x67, 0x66, 0x0a, 0x4f, 0x6e, 0x43, 0x87, 0xfa, 0xbe, 0x15, 0x72, 0xca, + 0x99, 0x35, 0x3a, 0xe3, 0x2c, 0xc4, 0x78, 0x5c, 0x37, 0x5b, 0xd4, 0xf7, 0x9f, 0x88, 0xd9, 0xa1, + 0x98, 0x34, 0xec, 0xf8, 0x6b, 0x62, 0xa8, 0x10, 0x02, 0x15, 0x9b, 0x72, 0x8a, 0xd6, 0x58, 0x37, + 0xf1, 0x59, 0xcc, 0xf9, 0x94, 0x4f, 0xd5, 0x19, 0xf1, 0x99, 0x5c, 0x85, 0xda, 0x94, 0x39, 0x93, + 0x29, 0xc7, 0x63, 0x95, 0x4d, 0x35, 0x12, 0x86, 0xf7, 0x03, 0xef, 0x94, 0x61, 0xb6, 0x68, 0x98, + 0x72, 0x60, 0xfc, 0x4d, 0x83, 0x4b, 0x17, 0xc2, 0x4b, 0xac, 0x3b, 0xa5, 0xe1, 0x34, 0x7a, 0x97, + 0x78, 0x26, 0xef, 0x88, 0x75, 0xa9, 0xcd, 0x02, 0x95, 0xc5, 0x5a, 0xea, 0xc4, 0x87, 0x38, 0xa9, + 0x0e, 0xaa, 0x20, 0xe4, 0x11, 0x74, 0x67, 0x34, 0xe4, 0x96, 0x8c, 0x02, 0x0b, 0xb3, 0x54, 0x39, + 0x13, 0x99, 0x8f, 0x69, 0x14, 0x2d, 0xc2, 0x39, 0x95, 0x7a, 0x7b, 0x96, 0x99, 0x25, 0x87, 0xb0, + 0x31, 0x3a, 0xfb, 0x25, 0x75, 0xb9, 0xe3, 0x32, 0xeb, 0x82, 0xcd, 0x3b, 0x6a, 0xa9, 0x47, 0xa7, + 0x8e, 0xcd, 0xdc, 0x71, 0x64, 0xec, 0xcb, 0xb1, 0x4a, 0xfc, 0x31, 0x42, 0xe3, 0x10, 0xda, 0xd9, + 0x5c, 0x40, 0xda, 0x50, 0xe2, 0x4b, 0x75, 0xc2, 0x12, 0x5f, 0x92, 0xdb, 0x50, 0x11, 0xcb, 0xe1, + 0xe9, 0xda, 0x71, 0x32, 0x55, 0xe8, 0xa7, 0x67, 0x3e, 0x33, 0x51, 0x6e, 0x18, 0xb1, 0xa7, 0xc6, + 0x81, 0x9b, 0x5f, 0xcb, 0xd8, 0x81, 0x4e, 0x2e, 0x89, 0xa4, 0x3e, 0x8b, 0x96, 0xfe, 0x2c, 0x46, + 0x07, 0x5a, 0x99, 0xdc, 0x61, 0x7c, 0x56, 0x85, 0x86, 0xc9, 0x42, 0x5f, 0x38, 0x1d, 0xd9, 0x07, + 0x9d, 0x2d, 0xc7, 0x4c, 0xa6, 0x6d, 0x2d, 0x97, 0x14, 0x25, 0xe6, 0x51, 0x24, 0x17, 0xe9, 0x23, + 0x06, 0x93, 0x9d, 0x0c, 0xe5, 0x5c, 0xce, 0x2b, 0xa5, 0x39, 0xe7, 0x4e, 0x96, 0x73, 0x36, 0x72, + 0xd8, 0x1c, 0xe9, 0xec, 0x64, 0x48, 0x27, 0xbf, 0x70, 0x86, 0x75, 0xee, 0x15, 0xb0, 0x4e, 0x7e, + 0xfb, 0x2b, 0x68, 0xe7, 0x5e, 0x01, 0xed, 0xf4, 0x2e, 0xbc, 0xab, 0x90, 0x77, 0xee, 0x64, 0x79, + 0x27, 0x7f, 0x9c, 0x1c, 0xf1, 0x7c, 0x58, 0x44, 0x3c, 0xd7, 0x73, 0x3a, 0x2b, 0x99, 0xe7, 0x83, + 0x0b, 0xcc, 0x73, 0x35, 0xa7, 0x5a, 0x40, 0x3d, 0xf7, 0x32, 0x9c, 0x00, 0x85, 0x67, 0x2b, 0x26, + 0x05, 0xf2, 0xfd, 0x8b, 0xac, 0x75, 0x2d, 0xff, 0x69, 0x8b, 0x68, 0x6b, 0x37, 0x47, 0x5b, 0x57, + 0xf2, 0xbb, 0xcc, 0xf1, 0x56, 0xc2, 0x3e, 0x3b, 0x22, 0x3f, 0xe4, 0x3c, 0x4d, 0xe4, 0x12, 0x16, + 0x04, 0x5e, 0xa0, 0x12, 0xbb, 0x1c, 0x18, 0xdb, 0x22, 0x63, 0x25, 0xfe, 0xf5, 0x1a, 0xa6, 0x42, + 0xa7, 0x4f, 0x79, 0x97, 0xf1, 0x85, 0x96, 0xe8, 0x62, 0xe4, 0xa7, 0xb3, 0x9d, 0xae, 0xb2, 0x5d, + 0x8a, 0xc0, 0x4a, 0x59, 0x02, 0xdb, 0x82, 0xa6, 0xc8, 0xa9, 0x39, 0x6e, 0xa2, 0x7e, 0xc4, 0x4d, + 0xe4, 0x6d, 0xb8, 0x84, 0xf9, 0x48, 0xd2, 0x9c, 0x0a, 0xc4, 0x0a, 0x06, 0x62, 0x47, 0x08, 0xa4, + 0xc5, 0x64, 0xa2, 0x7c, 0x17, 0x2e, 0xa7, 0xb0, 0x62, 0x5d, 0xcc, 0x85, 0x32, 0x49, 0x77, 0x63, + 0xf4, 0x81, 0xef, 0x1f, 0xd2, 0x70, 0x6a, 0xfc, 0x24, 0x31, 0x50, 0xc2, 0x7b, 0x04, 0x2a, 0x63, + 0xcf, 0x96, 0xe7, 0x6e, 0x99, 0xf8, 0x2c, 0xb8, 0x70, 0xe6, 0x4d, 0x70, 0x73, 0xba, 0x29, 0x1e, + 0x05, 0x2a, 0x0e, 0x25, 0x5d, 0xc6, 0x8c, 0xf1, 0x6b, 0x2d, 0x59, 0x2f, 0xa1, 0xc2, 0x22, 0xd6, + 0xd2, 0xfe, 0x17, 0xd6, 0x2a, 0x7d, 0x3d, 0xd6, 0x32, 0xce, 0xb5, 0xe4, 0x93, 0xc5, 0x7c, 0xf4, + 0x66, 0x47, 0x14, 0xde, 0xe3, 0xb8, 0x36, 0x5b, 0xa2, 0x49, 0xcb, 0xa6, 0x1c, 0x44, 0xa5, 0x42, + 0x0d, 0xcd, 0x9c, 0x2d, 0x15, 0xea, 0x38, 0x27, 0x07, 0xe4, 0x16, 0xf2, 0x98, 0xf7, 0x5c, 0x85, + 0x6a, 0x6b, 0xa0, 0x0a, 0xfa, 0x63, 0x31, 0x69, 0x4a, 0x59, 0x2a, 0xdb, 0xea, 0x19, 0x12, 0xbc, + 0x01, 0xba, 0xd8, 0x68, 0xe8, 0xd3, 0x31, 0xc3, 0xc8, 0xd3, 0xcd, 0x64, 0xc2, 0x78, 0x0a, 0xe4, + 0x62, 0xc4, 0x93, 0x8f, 0xa0, 0xc6, 0x4e, 0x99, 0xcb, 0x85, 0xc5, 0x85, 0xd1, 0xd6, 0x63, 0xda, + 0x61, 0x2e, 0x1f, 0xf6, 0x84, 0xa9, 0xfe, 0xfe, 0x6a, 0xab, 0x2b, 0x31, 0x77, 0xbc, 0xb9, 0xc3, + 0xd9, 0xdc, 0xe7, 0x67, 0xa6, 0xd2, 0x32, 0xfe, 0xa9, 0x09, 0x36, 0xc8, 0x64, 0x83, 0x42, 0xe3, + 0x45, 0x2e, 0x5f, 0x4a, 0x11, 0xfc, 0x57, 0x33, 0xe8, 0xb7, 0x01, 0x26, 0x34, 0xb4, 0x5e, 0x52, + 0x97, 0x33, 0x5b, 0x59, 0x55, 0x9f, 0xd0, 0xf0, 0xe7, 0x38, 0x21, 0xaa, 0x21, 0x21, 0x5e, 0x84, + 0xcc, 0x46, 0xf3, 0x96, 0xcd, 0xfa, 0x84, 0x86, 0xcf, 0x42, 0x66, 0xa7, 0xce, 0x56, 0x7f, 0x93, + 0xb3, 0x65, 0xed, 0xd9, 0xc8, 0xdb, 0xf3, 0xdf, 0x29, 0x5f, 0x4e, 0xc8, 0xf2, 0x9b, 0x71, 0xf6, + 0x7f, 0x68, 0xa2, 0x4e, 0xc8, 0xa6, 0x64, 0x72, 0x04, 0x97, 0xe2, 0x98, 0xb2, 0x16, 0x18, 0x6b, + 0x91, 0x57, 0xbd, 0x3e, 0x14, 0xbb, 0xa7, 0xd9, 0xe9, 0x90, 0x7c, 0x02, 0xd7, 0x72, 0x19, 0x21, + 0x5e, 0xb0, 0xf4, 0xda, 0xc4, 0x70, 0x25, 0x9b, 0x18, 0xa2, 0xf5, 0x12, 0x6b, 0x94, 0xdf, 0xc8, + 0xcb, 0xbf, 0x23, 0x0a, 0xac, 0x34, 0x99, 0x14, 0x7d, 0x53, 0xe3, 0x37, 0x1a, 0x74, 0x72, 0x1b, + 0x22, 0xdb, 0x50, 0x95, 0x7c, 0xa6, 0x65, 0xda, 0x58, 0xb4, 0x98, 0xda, 0xb3, 0x04, 0x90, 0xf7, + 0xa1, 0xc1, 0x54, 0xad, 0xa7, 0x0e, 0x79, 0x25, 0x57, 0x02, 0x2a, 0x7c, 0x0c, 0x23, 0xdf, 0x03, + 0x3d, 0x36, 0x5d, 0xae, 0xce, 0x8f, 0x2d, 0xad, 0x94, 0x12, 0xa0, 0xf1, 0x00, 0x9a, 0xa9, 0xd7, + 0x93, 0x6f, 0x81, 0x3e, 0xa7, 0x4b, 0x55, 0xac, 0xcb, 0xf2, 0xad, 0x31, 0xa7, 0x4b, 0xac, 0xd3, + 0xc9, 0x35, 0xa8, 0x0b, 0xe1, 0x84, 0x4a, 0xc3, 0x97, 0xcd, 0xda, 0x9c, 0x2e, 0x7f, 0x4c, 0x43, + 0x63, 0x07, 0xda, 0xd9, 0x6d, 0x45, 0xd0, 0x88, 0x10, 0x25, 0xf4, 0x60, 0xc2, 0x8c, 0xbb, 0xd0, + 0xc9, 0xed, 0x86, 0x18, 0xd0, 0xf2, 0x17, 0x23, 0xeb, 0x05, 0x3b, 0xb3, 0x70, 0xbb, 0xe8, 0x26, + 0xba, 0xd9, 0xf4, 0x17, 0xa3, 0x8f, 0xd9, 0x99, 0xa8, 0x47, 0x43, 0xe3, 0x09, 0xb4, 0xb3, 0x65, + 0xb4, 0x48, 0x99, 0x81, 0xb7, 0x70, 0x6d, 0x5c, 0xbf, 0x6a, 0xca, 0x81, 0xe8, 0xc4, 0x4f, 0x3d, + 0xe9, 0x19, 0xe9, 0xba, 0xf9, 0xc4, 0xe3, 0x2c, 0x55, 0x7c, 0x4b, 0x8c, 0xe1, 0x40, 0x15, 0xbf, + 0xb9, 0xf8, 0x7e, 0x58, 0x10, 0x2b, 0x0a, 0x16, 0xcf, 0xe4, 0x31, 0x00, 0xe5, 0x3c, 0x70, 0x46, + 0x8b, 0x64, 0xb9, 0xf6, 0x40, 0x5e, 0x8f, 0x0c, 0x3e, 0x3e, 0x39, 0xa6, 0x4e, 0x30, 0xbc, 0xa1, + 0x7c, 0x65, 0x23, 0x41, 0xa6, 0xfc, 0x25, 0xa5, 0x6f, 0xfc, 0xaa, 0x0a, 0x35, 0xd9, 0x3e, 0x90, + 0x41, 0xb6, 0x39, 0x15, 0xab, 0xaa, 0x4d, 0xca, 0x59, 0xb5, 0xc7, 0x98, 0xf1, 0x6f, 0xe7, 0x3b, + 0xbc, 0x61, 0xf3, 0xfc, 0xd5, 0x56, 0x1d, 0xd9, 0xf2, 0xe8, 0x61, 0xd2, 0xee, 0xad, 0xea, 0x86, + 0xa2, 0xde, 0xb2, 0xf2, 0xb5, 0x7b, 0xcb, 0x6b, 0x50, 0x77, 0x17, 0x73, 0x8b, 0x2f, 0x43, 0x95, + 0x6d, 0x6a, 0xee, 0x62, 0xfe, 0x74, 0x89, 0x5e, 0xc2, 0x3d, 0x4e, 0x67, 0x28, 0x92, 0xb9, 0xa6, + 0x81, 0x13, 0x42, 0xb8, 0x0f, 0xad, 0x54, 0x51, 0xe1, 0xd8, 0xaa, 0x38, 0x6d, 0xa7, 0x9d, 0xfd, + 0xe8, 0xa1, 0x3a, 0x65, 0x33, 0x2e, 0x32, 0x8e, 0x6c, 0xb2, 0x9d, 0x6d, 0xa5, 0xb0, 0x16, 0x69, + 0x60, 0x48, 0xa5, 0xba, 0x25, 0x51, 0x89, 0x88, 0x0d, 0x88, 0x20, 0x93, 0x10, 0x1d, 0x21, 0x0d, + 0x31, 0x81, 0xc2, 0xb7, 0xa0, 0x93, 0xd0, 0xb9, 0x84, 0x80, 0x5c, 0x25, 0x99, 0x46, 0xe0, 0x7b, + 0xb0, 0xe1, 0xb2, 0x25, 0xb7, 0xf2, 0xe8, 0x26, 0xa2, 0x89, 0x90, 0x9d, 0x64, 0x35, 0xbe, 0x0b, + 0xed, 0x24, 0x15, 0x21, 0x76, 0x5d, 0x36, 0xb4, 0xf1, 0x2c, 0xc2, 0xae, 0x43, 0x23, 0x2e, 0xa6, + 0x5a, 0x08, 0xa8, 0x53, 0x59, 0x43, 0xc5, 0xe5, 0x59, 0xc0, 0xc2, 0xc5, 0x8c, 0xab, 0x45, 0xda, + 0x88, 0xc1, 0xf2, 0xcc, 0x94, 0xf3, 0x88, 0xbd, 0x05, 0xad, 0x28, 0xba, 0x25, 0xae, 0x83, 0xb8, + 0xf5, 0x68, 0x12, 0x41, 0x3b, 0xd0, 0xf5, 0x03, 0xcf, 0xf7, 0x42, 0x16, 0x58, 0xd4, 0xb6, 0x03, + 0x16, 0x86, 0xbd, 0xae, 0x5c, 0x2f, 0x9a, 0x3f, 0x90, 0xd3, 0xc6, 0xfb, 0x50, 0x8f, 0xaa, 0xc4, + 0x0d, 0xa8, 0x0e, 0xe3, 0x4c, 0x54, 0x31, 0xe5, 0x40, 0xf0, 0xd0, 0x81, 0xef, 0xab, 0x3b, 0x11, + 0xf1, 0x68, 0xfc, 0x02, 0xea, 0xea, 0x83, 0x15, 0x76, 0xca, 0x3f, 0x84, 0x75, 0x9f, 0x06, 0xe2, + 0x18, 0xe9, 0x7e, 0x39, 0xea, 0x43, 0x8e, 0x69, 0xc0, 0x9f, 0x30, 0x9e, 0x69, 0x9b, 0x9b, 0x88, + 0x97, 0x53, 0xc6, 0x3d, 0x68, 0x65, 0x30, 0x62, 0x5b, 0xe8, 0x47, 0x51, 0x50, 0xe3, 0x20, 0x7e, + 0x73, 0x29, 0x79, 0xb3, 0x71, 0x1f, 0xf4, 0xf8, 0xdb, 0x88, 0x72, 0x39, 0x3a, 0xba, 0xa6, 0xcc, + 0x2d, 0x87, 0x78, 0x15, 0xe0, 0xbd, 0x64, 0x81, 0x8a, 0x09, 0x39, 0x30, 0x9e, 0xa5, 0x92, 0x90, + 0x64, 0x05, 0x72, 0x07, 0xea, 0x2a, 0x09, 0xa9, 0xa8, 0x8c, 0x9a, 0xfe, 0x63, 0xcc, 0x42, 0x51, + 0xd3, 0x2f, 0x73, 0x52, 0xb2, 0x6c, 0x29, 0xbd, 0xec, 0x0c, 0x1a, 0x51, 0xa2, 0xc9, 0x66, 0x63, + 0xb9, 0x62, 0x37, 0x9f, 0x8d, 0xd5, 0xa2, 0x09, 0x50, 0x78, 0x47, 0xe8, 0x4c, 0x5c, 0x66, 0x5b, + 0x49, 0x08, 0xe1, 0x3b, 0x1a, 0x66, 0x47, 0x0a, 0x1e, 0x47, 0xf1, 0x62, 0xbc, 0x07, 0x35, 0xb9, + 0xb7, 0xc2, 0xf4, 0x55, 0x44, 0x49, 0x7f, 0xd4, 0xa0, 0x11, 0xe5, 0xe9, 0x42, 0xa5, 0xcc, 0xa6, + 0x4b, 0x5f, 0x75, 0xd3, 0xff, 0xff, 0xc4, 0x73, 0x07, 0x88, 0xcc, 0x2f, 0xa7, 0x1e, 0x77, 0xdc, + 0x89, 0x25, 0x6d, 0x2d, 0x73, 0x50, 0x17, 0x25, 0x27, 0x28, 0x38, 0x16, 0xf3, 0x6f, 0xdf, 0x82, + 0x66, 0xea, 0xee, 0x82, 0xd4, 0xa1, 0xfc, 0x09, 0x7b, 0xd9, 0x5d, 0x23, 0x4d, 0xa8, 0x9b, 0x0c, + 0x3b, 0xd1, 0xae, 0xb6, 0xf7, 0x59, 0x15, 0x3a, 0x07, 0xc3, 0x07, 0x47, 0x07, 0xbe, 0x3f, 0x73, + 0xc6, 0x14, 0x5b, 0x97, 0x5d, 0xa8, 0x60, 0xf7, 0x56, 0x70, 0x4b, 0xdd, 0x2f, 0xba, 0x46, 0x20, + 0x7b, 0x50, 0xc5, 0x26, 0x8e, 0x14, 0x5d, 0x56, 0xf7, 0x0b, 0x6f, 0x13, 0xc4, 0x4b, 0x64, 0x9b, + 0x77, 0xf1, 0xce, 0xba, 0x5f, 0x74, 0xa5, 0x40, 0x3e, 0x02, 0x3d, 0xe9, 0xae, 0x56, 0xdd, 0x5c, + 0xf7, 0x57, 0x5e, 0x2e, 0x08, 0xfd, 0xa4, 0x02, 0x5d, 0x75, 0x01, 0xdb, 0x5f, 0xd9, 0x85, 0x93, + 0x7d, 0xa8, 0x47, 0xb5, 0x7b, 0xf1, 0xdd, 0x72, 0x7f, 0x45, 0xe3, 0x2f, 0xcc, 0x23, 0x1b, 0xa6, + 0xa2, 0x0b, 0xf0, 0x7e, 0xe1, 0xed, 0x04, 0xb9, 0x0b, 0x35, 0x55, 0x44, 0x15, 0xde, 0x2f, 0xf7, + 0x8b, 0xdb, 0x77, 0x71, 0xc8, 0xa4, 0x65, 0x5c, 0x75, 0x49, 0xdf, 0x5f, 0x79, 0x8d, 0x42, 0x0e, + 0x00, 0x52, 0x7d, 0xcf, 0xca, 0xdb, 0xf7, 0xfe, 0xea, 0xeb, 0x11, 0x72, 0x1f, 0x1a, 0xc9, 0x95, + 0x57, 0xf1, 0x7d, 0x7a, 0x7f, 0xd5, 0x8d, 0xc5, 0xf0, 0xc6, 0xbf, 0xfe, 0xb2, 0xa9, 0xfd, 0xf6, + 0x7c, 0x53, 0xfb, 0xe2, 0x7c, 0x53, 0xfb, 0xf2, 0x7c, 0x53, 0xfb, 0xc3, 0xf9, 0xa6, 0xf6, 0xe7, + 0xf3, 0x4d, 0xed, 0x77, 0x7f, 0xdd, 0xd4, 0x46, 0x35, 0x8c, 0x91, 0x0f, 0xfe, 0x13, 0x00, 0x00, + 0xff, 0xff, 0x92, 0xed, 0x9f, 0xca, 0x3f, 0x1a, 0x00, 0x00, } diff --git a/abci/types/types.proto b/abci/types/types.proto index 8a2da5b47..8f9dda832 100644 --- a/abci/types/types.proto +++ b/abci/types/types.proto @@ -81,8 +81,14 @@ message RequestBeginBlock { repeated Evidence byzantine_validators = 4 [(gogoproto.nullable)=false]; } +enum CheckTxType { + New = 0; + Recheck = 1; +} + message RequestCheckTx { bytes tx = 1; + CheckTxType type = 2; } message RequestDeliverTx { diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 724dd056b..bbb5b6678 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -565,7 +565,7 @@ func TestMockProxyApp(t *testing.T) { mock.SetResponseCallback(proxyCb) someTx := []byte("tx") - mock.DeliverTxAsync(someTx) + mock.DeliverTxAsync(abci.RequestDeliverTx{Tx: someTx}) }) assert.True(t, validTxs == 1) assert.True(t, invalidTxs == 0) diff --git a/docs/spec/abci/abci.md b/docs/spec/abci/abci.md index b7b2e09fe..abab7f548 100644 --- a/docs/spec/abci/abci.md +++ b/docs/spec/abci/abci.md @@ -296,6 +296,12 @@ Commit are included in the header of the next block. - **Request**: - `Tx ([]byte)`: The request transaction bytes + - `Type (CheckTxType)`: What type of `CheckTx` request is this? At present, + there are two possible values: `CheckTx_Unchecked` (the default, which says + that a full check is required), and `CheckTx_Checked` (when the mempool is + initiating a normal recheck of a transaction). + - `AdditionalData ([]byte)`: Reserved for future use. See + [here](https://github.com/tendermint/tendermint/issues/2127#issuecomment-456661420). - **Response**: - `Code (uint32)`: Response code - `Data ([]byte)`: Result bytes, if any. diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 0d1f3c5b1..4042e9b4b 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -279,7 +279,7 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t return err } - reqRes := mem.proxyAppConn.CheckTxAsync(tx) + reqRes := mem.proxyAppConn.CheckTxAsync(abci.RequestCheckTx{Tx: tx}) reqRes.SetCallback(mem.reqResCb(tx, txInfo.SenderID, cb)) return nil @@ -591,7 +591,10 @@ func (mem *CListMempool) recheckTxs() { // NOTE: globalCb may be called concurrently. for e := mem.txs.Front(); e != nil; e = e.Next() { memTx := e.Value.(*mempoolTx) - mem.proxyAppConn.CheckTxAsync(memTx.tx) + mem.proxyAppConn.CheckTxAsync(abci.RequestCheckTx{ + Tx: memTx.tx, + Type: abci.CheckTxType_Recheck, + }) } mem.proxyAppConn.FlushAsync() diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index db6e800b8..90d0ed1ae 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -314,7 +314,7 @@ func TestSerialReap(t *testing.T) { for i := start; i < end; i++ { txBytes := make([]byte, 8) binary.BigEndian.PutUint64(txBytes, uint64(i)) - res, err := appConnCon.DeliverTxSync(txBytes) + res, err := appConnCon.DeliverTxSync(abci.RequestDeliverTx{Tx: txBytes}) if err != nil { t.Errorf("Client error committing tx: %v", err) } @@ -522,7 +522,7 @@ func TestMempoolTxsBytes(t *testing.T) { err = appConnCon.Start() require.Nil(t, err) defer appConnCon.Stop() - res, err := appConnCon.DeliverTxSync(txBytes) + res, err := appConnCon.DeliverTxSync(abci.RequestDeliverTx{Tx: txBytes}) require.NoError(t, err) require.EqualValues(t, 0, res.Code) res2, err := appConnCon.CommitSync() diff --git a/proxy/app_conn.go b/proxy/app_conn.go index 2f792671e..1698ab52f 100644 --- a/proxy/app_conn.go +++ b/proxy/app_conn.go @@ -15,7 +15,7 @@ type AppConnConsensus interface { InitChainSync(types.RequestInitChain) (*types.ResponseInitChain, error) BeginBlockSync(types.RequestBeginBlock) (*types.ResponseBeginBlock, error) - DeliverTxAsync(tx []byte) *abcicli.ReqRes + DeliverTxAsync(types.RequestDeliverTx) *abcicli.ReqRes EndBlockSync(types.RequestEndBlock) (*types.ResponseEndBlock, error) CommitSync() (*types.ResponseCommit, error) } @@ -24,7 +24,7 @@ type AppConnMempool interface { SetResponseCallback(abcicli.Callback) Error() error - CheckTxAsync(tx []byte) *abcicli.ReqRes + CheckTxAsync(types.RequestCheckTx) *abcicli.ReqRes FlushAsync() *abcicli.ReqRes FlushSync() error @@ -69,8 +69,8 @@ func (app *appConnConsensus) BeginBlockSync(req types.RequestBeginBlock) (*types return app.appConn.BeginBlockSync(req) } -func (app *appConnConsensus) DeliverTxAsync(tx []byte) *abcicli.ReqRes { - return app.appConn.DeliverTxAsync(tx) +func (app *appConnConsensus) DeliverTxAsync(req types.RequestDeliverTx) *abcicli.ReqRes { + return app.appConn.DeliverTxAsync(req) } func (app *appConnConsensus) EndBlockSync(req types.RequestEndBlock) (*types.ResponseEndBlock, error) { @@ -110,8 +110,8 @@ func (app *appConnMempool) FlushSync() error { return app.appConn.FlushSync() } -func (app *appConnMempool) CheckTxAsync(tx []byte) *abcicli.ReqRes { - return app.appConn.CheckTxAsync(tx) +func (app *appConnMempool) CheckTxAsync(req types.RequestCheckTx) *abcicli.ReqRes { + return app.appConn.CheckTxAsync(req) } //------------------------------------------------ diff --git a/state/execution.go b/state/execution.go index 7e49a9ad8..fd75b2959 100644 --- a/state/execution.go +++ b/state/execution.go @@ -284,7 +284,7 @@ func execBlockOnProxyApp( // Run txs of block. for _, tx := range block.Txs { - proxyAppConn.DeliverTxAsync(tx) + proxyAppConn.DeliverTxAsync(abci.RequestDeliverTx{Tx: tx}) if err := proxyAppConn.Error(); err != nil { return nil, err } From d0414768195807044fa8614b07f4e0035fd5f9e1 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 2 Jul 2019 21:19:58 +0400 Subject: [PATCH 088/211] p2p: dial addrs which came from seed instead of calling ensurePeers (#3762) Calling ensurePeers outside of ensurePeersRoutine can lead to nodes disconnecting from us due to "sent next PEX request too soon" error. Solution is to just dial addrs we got from src instead of calling ensurePeers. Refs #2093 Fixes #3338 --- CHANGELOG_PENDING.md | 2 ++ p2p/pex/pex_reactor.go | 29 ++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 9c94c55e7..fa81d9bbb 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -31,3 +31,5 @@ program](https://hackerone.com/tendermint). - [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) ### BUG FIXES: +- [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling + ensurePeers outside of ensurePeersRoutine diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index e77fa8eaa..20862d323 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -340,6 +340,15 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { if err != nil { return err } + + srcIsSeed := false + for _, seedAddr := range r.seedAddrs { + if seedAddr.Equals(srcAddr) { + srcIsSeed = true + break + } + } + for _, netAddr := range addrs { // Validate netAddr. Disconnect from a peer if it sends us invalid data. if netAddr == nil { @@ -365,13 +374,23 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { } // If this address came from a seed node, try to connect to it without - // waiting. - for _, seedAddr := range r.seedAddrs { - if seedAddr.Equals(srcAddr) { - r.ensurePeers() - } + // waiting (#2093) + if srcIsSeed { + r.Logger.Info("Will dial address, which came from seed", "addr", netAddr, "seed", srcAddr) + go func(addr *p2p.NetAddress) { + err := r.dialPeer(addr) + if err != nil { + switch err.(type) { + case errMaxAttemptsToDial, errTooEarlyToDial: + r.Logger.Debug(err.Error(), "addr", addr) + default: + r.Logger.Error(err.Error(), "addr", addr) + } + } + }(netAddr) } } + return nil } From e645442c9be7694a09fccfde206ff715ee462824 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 2 Jul 2019 22:01:29 +0400 Subject: [PATCH 089/211] abci: minor cleanups in the socket client (#3758) Follow up from #3512 Specifically: cli.conn.Close() need not be under the mutex (#3512 (comment)) call the reqRes callback after the resCb so they always happen in the same order (#3512) Fixes #3513 --- abci/client/socket_client.go | 22 +++++++--------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/abci/client/socket_client.go b/abci/client/socket_client.go index 16e39bf35..7b1e4cd8d 100644 --- a/abci/client/socket_client.go +++ b/abci/client/socket_client.go @@ -55,10 +55,6 @@ func NewSocketClient(addr string, mustConnect bool) *socketClient { } func (cli *socketClient) OnStart() error { - if err := cli.BaseService.OnStart(); err != nil { - return err - } - var err error var conn net.Conn RETRY_LOOP: @@ -82,15 +78,12 @@ RETRY_LOOP: } func (cli *socketClient) OnStop() { - cli.BaseService.OnStop() - - cli.mtx.Lock() - defer cli.mtx.Unlock() if cli.conn != nil { - // does this really need a mutex? cli.conn.Close() } + cli.mtx.Lock() + defer cli.mtx.Unlock() cli.flushQueue() } @@ -209,19 +202,18 @@ func (cli *socketClient) didRecvResponse(res *types.Response) error { reqres.Done() // Release waiters cli.reqSent.Remove(next) // Pop first item from linked list + // Notify client listener if set (global callback). + if cli.resCb != nil { + cli.resCb(reqres.Request, res) + } + // Notify reqRes listener if set (request specific callback). // NOTE: it is possible this callback isn't set on the reqres object. // at this point, in which case it will be called after, when it is set. - // TODO: should we move this after the resCb call so the order is always consistent? if cb := reqres.GetCallback(); cb != nil { cb(res) } - // Notify client listener if set (global callback). - if cli.resCb != nil { - cli.resCb(reqres.Request, res) - } - return nil } From f76684a05c9e179f8b5fcb528d90b74a1956b4b9 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 3 Jul 2019 17:17:59 +0400 Subject: [PATCH 090/211] node: allow registration of custom reactors while creating node (#3771) * change invocation of NewNode across * custom reactor name are prefixed with CUSTOM_ * upgate changelog pending * improve comments * node: refactor NewNode to use functional options --- CHANGELOG_PENDING.md | 4 ++++ node/node.go | 25 ++++++++++++++++++++++++- node/node_test.go | 34 +++++++++++++++++++++++++++++++++- p2p/mock/reactor.go | 23 +++++++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 p2p/mock/reactor.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index fa81d9bbb..5a747650e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -26,6 +26,10 @@ program](https://hackerone.com/tendermint). * P2P Protocol ### FEATURES: +- [node] Refactor `NewNode` to use functional options to make it more flexible + and extensible in the future. +- [node] [\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass + custom reactors to run inside Tendermint node (@ParthDesai) ### IMPROVEMENTS: - [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) diff --git a/node/node.go b/node/node.go index c992e2424..9beb0669f 100644 --- a/node/node.go +++ b/node/node.go @@ -47,6 +47,10 @@ import ( "github.com/tendermint/tendermint/version" ) +// CustomReactorNamePrefix is a prefix for all custom reactors to prevent +// clashes with built-in reactors. +const CustomReactorNamePrefix = "CUSTOM_" + //------------------------------------------------------------------------------ // DBContext specifies config information for loading a new DB. @@ -136,6 +140,18 @@ func DefaultMetricsProvider(config *cfg.InstrumentationConfig) MetricsProvider { } } +// Option sets a parameter for the node. +type Option func(*Node) + +// CustomReactors allows you to add custom reactors to the node's Switch. +func CustomReactors(reactors map[string]p2p.Reactor) Option { + return func(n *Node) { + for name, reactor := range reactors { + n.sw.AddReactor(CustomReactorNamePrefix+name, reactor) + } + } +} + //------------------------------------------------------------------------------ // Node is the highest level interface to a full Tendermint node. @@ -433,6 +449,7 @@ func createSwitch(config *cfg.Config, sw.AddReactor("BLOCKCHAIN", bcReactor) sw.AddReactor("CONSENSUS", consensusReactor) sw.AddReactor("EVIDENCE", evidenceReactor) + sw.SetNodeInfo(nodeInfo) sw.SetNodeKey(nodeKey) @@ -495,7 +512,8 @@ func NewNode(config *cfg.Config, genesisDocProvider GenesisDocProvider, dbProvider DBProvider, metricsProvider MetricsProvider, - logger log.Logger) (*Node, error) { + logger log.Logger, + options ...Option) (*Node, error) { blockStore, stateDB, err := initDBs(config, dbProvider) if err != nil { @@ -661,6 +679,11 @@ func NewNode(config *cfg.Config, eventBus: eventBus, } node.BaseService = *cmn.NewBaseService(logger, "Node", node) + + for _, option := range options { + option(node) + } + return node, nil } diff --git a/node/node_test.go b/node/node_test.go index ce4e82c2d..841a04686 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -21,6 +21,7 @@ import ( "github.com/tendermint/tendermint/libs/log" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" + p2pmock "github.com/tendermint/tendermint/p2p/mock" "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" @@ -100,7 +101,10 @@ func TestNodeDelayedStart(t *testing.T) { n.GenesisDoc().GenesisTime = now.Add(2 * time.Second) require.NoError(t, err) - n.Start() + err = n.Start() + require.NoError(t, err) + defer n.Stop() + startTime := tmtime.Now() assert.Equal(t, true, startTime.After(n.GenesisDoc().GenesisTime)) } @@ -279,6 +283,34 @@ func TestCreateProposalBlock(t *testing.T) { assert.NoError(t, err) } +func TestNodeNewNodeCustomReactors(t *testing.T) { + config := cfg.ResetTestRoot("node_new_node_custom_reactors_test") + defer os.RemoveAll(config.RootDir) + + cr := p2pmock.NewReactor() + + nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) + require.NoError(t, err) + + n, err := NewNode(config, + privval.LoadOrGenFilePV(config.PrivValidatorKeyFile(), config.PrivValidatorStateFile()), + nodeKey, + proxy.DefaultClientCreator(config.ProxyApp, config.ABCI, config.DBDir()), + DefaultGenesisDocProviderFunc(config), + DefaultDBProvider, + DefaultMetricsProvider(config.Instrumentation), + log.TestingLogger(), + CustomReactors(map[string]p2p.Reactor{"FOO": cr}), + ) + require.NoError(t, err) + + err = n.Start() + require.NoError(t, err) + defer n.Stop() + + assert.True(t, cr.IsRunning()) +} + func state(nVals int, height int64) (sm.State, dbm.DB) { vals := make([]types.GenesisValidator, nVals) for i := 0; i < nVals; i++ { diff --git a/p2p/mock/reactor.go b/p2p/mock/reactor.go new file mode 100644 index 000000000..cfce12bd1 --- /dev/null +++ b/p2p/mock/reactor.go @@ -0,0 +1,23 @@ +package mock + +import ( + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/p2p/conn" +) + +type Reactor struct { + p2p.BaseReactor +} + +func NewReactor() *Reactor { + r := &Reactor{} + r.BaseReactor = *p2p.NewBaseReactor("Reactor", r) + r.SetLogger(log.TestingLogger()) + return r +} + +func (r *Reactor) GetChannels() []*conn.ChannelDescriptor { return []*conn.ChannelDescriptor{} } +func (r *Reactor) AddPeer(peer p2p.Peer) {} +func (r *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) {} +func (r *Reactor) Receive(chID byte, peer p2p.Peer, msgBytes []byte) {} From 94e0176ac24090d8048c1fa71b51e84ad8e77701 Mon Sep 17 00:00:00 2001 From: Jay Namsayin <31609693+jim380@users.noreply.github.com> Date: Thu, 4 Jul 2019 00:26:13 -0700 Subject: [PATCH 091/211] behaviour: return correct reason in MessageOutOfOrder (#3772) --- CHANGELOG_PENDING.md | 2 ++ behaviour/peer_behaviour.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 5a747650e..9c4c8b684 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -37,3 +37,5 @@ program](https://hackerone.com/tendermint). ### BUG FIXES: - [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling ensurePeers outside of ensurePeersRoutine +- [behaviour] Return correct reason in MessageOutOfOrder (@jim380) + diff --git a/behaviour/peer_behaviour.go b/behaviour/peer_behaviour.go index 36630f46b..f7cfd00f0 100644 --- a/behaviour/peer_behaviour.go +++ b/behaviour/peer_behaviour.go @@ -27,7 +27,7 @@ type messageOutOfOrder struct { // MessageOutOfOrder returns a messagOutOfOrder PeerBehaviour. func MessageOutOfOrder(peerID p2p.ID, explanation string) PeerBehaviour { - return PeerBehaviour{peerID: peerID, reason: badMessage{explanation}} + return PeerBehaviour{peerID: peerID, reason: messageOutOfOrder{explanation}} } type consensusVote struct { From 1d5fcc22814389abfc3f461d94ca3afe3307fbfa Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Mon, 8 Jul 2019 19:45:52 +0200 Subject: [PATCH 092/211] adr: [43] blockchain riri-org (#3753) * [adr] First draft on adr-042 * fix diagram urls * Update docs/architecture/adr-042-blockchain-riri-org.md Co-Authored-By: Marko * Update docs/architecture/adr-042-blockchain-riri-org.md Co-Authored-By: Marko * Update docs/architecture/adr-042-blockchain-riri-org.md Co-Authored-By: Marko * add go syntax highlight * more highlighting * consistency fixes * Add references * new adr number * Fixes based on feedback * aditional state info * Add details on getSchedule * replace spaces with tabs * fixes based on feedback * add clarity around r.msgs * clarify block processing * fix off by one error * additional details on ioRoutine * Update docs/architecture/adr-043-blockchain-riri-org.md Co-Authored-By: Anca Zamfir --- .../adr-043-blockchain-riri-org.md | 391 ++++++++++++++++++ .../img/blockchain-reactor-v1.png | Bin 0 -> 124042 bytes .../img/blockchain-reactor-v2.png | Bin 0 -> 120617 bytes 3 files changed, 391 insertions(+) create mode 100644 docs/architecture/adr-043-blockchain-riri-org.md create mode 100644 docs/architecture/img/blockchain-reactor-v1.png create mode 100644 docs/architecture/img/blockchain-reactor-v2.png diff --git a/docs/architecture/adr-043-blockchain-riri-org.md b/docs/architecture/adr-043-blockchain-riri-org.md new file mode 100644 index 000000000..3cdf6e31e --- /dev/null +++ b/docs/architecture/adr-043-blockchain-riri-org.md @@ -0,0 +1,391 @@ +# ADR 043: Blockhchain Reactor Riri-Org + +## Changelog +* 18-06-2019: Initial draft +* 08-07-2019: Reviewed + +## Context + +The blockchain reactor is responsible for two high level processes:sending/receiving blocks from peers and FastSync-ing blocks to catch upnode who is far behind. The goal of [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) was to refactor these two processes by separating business logic currently wrapped up in go-channels into pure `handle*` functions. While the ADR specified what the final form of the reactor might look like it lacked guidance on intermediary steps to get there. +The following diagram illustrates the state of the [blockchain-reorg](https://github.com/tendermint/tendermint/pull/35610) reactor which will be referred to as `v1`. + +![v1 Blockchain Reactor Architecture +Diagram](https://github.com/tendermint/tendermint/blob/f9e556481654a24aeb689bdadaf5eab3ccd66829/docs/architecture/img/blockchain-reactor-v1.png) + +While `v1` of the blockchain reactor has shown significant improvements in terms of simplifying the concurrency model, the current PR has run into few roadblocks. + +* The current PR large and difficult to review. +* Block gossiping and fast sync processes are highly coupled to the shared `Pool` data structure. +* Peer communication is spread over multiple components creating complex dependency graph which must be mocked out during testing. +* Timeouts modeled as stateful tickers introduce non-determinism in tests + +This ADR is meant to specify the missing components and control necessary to achieve [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md). + +## Decision + +Partition the responsibilities of the blockchain reactor into a set of components which communicate exclusively with events. Events will contain timestamps allowing each component to track time as internal state. The internal state will be mutated by a set of `handle*` which will produce event(s). The integration between components will happen in the reactor and reactor tests will then become integration tests between components. This design will be known as `v2`. + +![v2 Blockchain Reactor Architecture +Diagram](https://github.com/tendermint/tendermint/blob/f9e556481654a24aeb689bdadaf5eab3ccd66829/docs/architecture/img/blockchain-reactor-v2.png) + +### Reactor changes in detail + +The reactor will include a demultiplexing routine which will send each message to each sub routine for independent processing. Each sub routine will then select the messages it's interested in and call the handle specific function specified in [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md). The demuxRoutine acts as "pacemaker" setting the time in which events are expected to be handled. + + +```go +func demuxRoutine(msgs, scheduleMsgs, processorMsgs, ioMsgs) { + timer := time.NewTicker(interval) + for { + select { + case <-timer.C: + now := evTimeCheck{time.Now()} + schedulerMsgs <- now + processorMsgs <- now + ioMsgs <- now + case msg:= <- msgs: + msg.time = time.Now() + // These channels should produce backpressure before + // being full to avoid starving each other + schedulerMsgs <- msg + processorMsgs <- msg + ioMesgs <- msg + if msg == stop { + break; + } + } + } +} + +func processRoutine(input chan Message, output chan Message) { + processor := NewProcessor(..) + for { + msg := <- input + switch msg := msg.(type) { + case bcBlockRequestMessage: + output <- processor.handleBlockRequest(msg)) + ... + case stop: + processor.stop() + break; + } +} + +func scheduleRoutine(input chan Message, output chan Message) { + schelduer = NewScheduler(...) + for { + msg := <-msgs + switch msg := input.(type) { + case bcBlockResponseMessage: + output <- scheduler.handleBlockResponse(msg) + ... + case stop: + schedule.stop() + break; + } + } +} +``` + +## Lifecycle management + +A set of routines for individual processes allow processes to run in parallel with clear lifecycle management. `Start`, `Stop`, and `AddPeer` hooks currently present in the reactor will delegate to the sub-routines allowing them to manage internal state independent without further coupling to the reactor. + +```go +func (r *BlockChainReactor) Start() { + r.msgs := make(chan Message, maxInFlight) + schedulerMsgs := make(chan Message) + processorMsgs := make(chan Message) + ioMsgs := make(chan Message) + + go processorRoutine(processorMsgs, r.msgs) + go scheduleRoutine(schedulerMsgs, r.msgs) + go ioRoutine(ioMsgs, r.msgs) + ... +} + +func (bcR *BlockchainReactor) Receive(...) { + ... + r.msgs <- msg + ... +} + +func (r *BlockchainReactor) Stop() { + ... + r.msgs <- stop + ... +} + +... +func (r *BlockchainReactor) Stop() { + ... + r.msgs <- stop + ... +} +... + +func (r *BlockchainReactor) AddPeer(peer p2p.Peer) { + ... + r.msgs <- bcAddPeerEv{peer.ID} + ... +} + +``` + +## IO handling +An io handling routine within the reactor will isolate peer communication. Message going through the ioRoutine will usually be one way, using `p2p` APIs. In the case in which the `p2p` API such as `trySend` return errors, the ioRoutine can funnel those message back to the demuxRoutine for distribution to the other routines. For instance errors from the ioRoutine can be consumed by the scheduler to inform better peer selection implementations. + +```go +func (r *BlockchainReacor) ioRoutine(ioMesgs chan Message, outMsgs chan Message) { + ... + for { + msg := <-ioMsgs + switch msg := msg.(type) { + case scBlockRequestMessage: + queued := r.sendBlockRequestToPeer(...) + if queued { + outMsgs <- ioSendQueued{...} + } + case scStatusRequestMessage + r.sendStatusRequestToPeer(...) + case bcPeerError + r.Swtich.StopPeerForError(msg.src) + ... + ... + case bcFinished + break; + } + } +} + +``` +### Processor Internals + +The processor is responsible for ordering, verifying and executing blocks. The Processor will maintain an internal cursor `height` refering to the last processed block. As a set of blocks arrive unordered, the Processor will check if it has `height+1` necessary to process the next block. The processor also maintains the map `blockPeers` of peers to height, to keep track of which peer provided the block at `height`. `blockPeers` can be used in`handleRemovePeer(...)` to reschedule all unprocessed blocks provided by a peer who has errored. + +```go +type Processor struct { + height int64 // the height cursor + state ... + blocks [height]*Block // keep a set of blocks in memory until they are processed + blockPeers [height]PeerID // keep track of which heights came from which peerID + lastTouch timestamp +} + +func (proc *Processor) handleBlockResponse(peerID, block) { + if block.height <= height || block[block.height] { + } else if blocks[block.height] { + return errDuplicateBlock{} + } else { + blocks[block.height] = block + } + + if blocks[height] && blocks[height+1] { + ... = state.Validators.VerifyCommit(...) + ... = store.SaveBlock(...) + state, err = blockExec.ApplyBlock(...) + ... + if err == nil { + delete blocks[height] + height++ + lastTouch = msg.time + return pcBlockProcessed{height-1} + } else { + ... // Delete all unprocessed block from the peer + return pcBlockProcessError{peerID, height} + } + } +} + +func (proc *Processor) handleRemovePeer(peerID) { + events = [] + // Delete all unprocessed blocks from peerID + for i = height; i < len(blocks); i++ { + if blockPeers[i] == peerID { + events = append(events, pcBlockReschedule{height}) + + delete block[height] + } + } + return events +} + +func handleTimeCheckEv(time) { + if time - lastTouch > timeout { + // Timeout the processor + ... + } +} +``` + +## Schedule + +The Schedule maintains the internal state used for scheduling blockRequestMessages based on some scheduling algorithm. The schedule needs to maintain state on: + +* The state `blockState` of every block seem up to height of maxHeight +* The set of peers and their peer state `peerState` +* which peers have which blocks +* which blocks have been requested from which peers + +```go +type blockState int + +const ( + blockStateNew = iota + blockStatePending, + blockStateReceived, + blockStateProcessed +) + +type schedule { + // a list of blocks in which blockState + blockStates map[height]blockState + + // a map of which blocks are available from which peers + blockPeers map[height]map[p2p.ID]scPeer + + // a map of peerID to schedule specific peer struct `scPeer` + peers map[p2p.ID]scPeer + + // a map of heights to the peer we are waiting for a response from + pending map[height]scPeer + + targetPending int // the number of blocks we want in blockStatePending + targetReceived int // the number of blocks we want in blockStateReceived + + peerTimeout int + peerMinSpeed int +} + +func (sc *schedule) numBlockInState(state blockState) uint32 { + num := 0 + for i := sc.minHeight(); i <= sc.maxHeight(); i++ { + if sc.blockState[i] == state { + num++ + } + } + return num +} + + +func (sc *schedule) popSchedule(maxRequest int) []scBlockRequestMessage { + // We only want to schedule requests such that we have less than sc.targetPending and sc.targetReceived + // This ensures we don't saturate the network or flood the processor with unprocessed blocks + todo := min(sc.targetPending - sc.numBlockInState(blockStatePending), sc.numBlockInState(blockStateReceived)) + events := []scBlockRequestMessage{} + for i := sc.minHeight(); i < sc.maxMaxHeight(); i++ { + if todo == 0 { + break + } + if blockStates[i] == blockStateNew { + peer = sc.selectPeer(blockPeers[i]) + sc.blockStates[i] = blockStatePending + sc.pending[i] = peer + events = append(events, scBlockRequestMessage{peerID: peer.peerID, height: i}) + todo-- + } + } + return events +} +... + +type scPeer struct { + peerID p2p.ID + numOustandingRequest int + lastTouched time.Time + monitor flow.Monitor +} + +``` + +# Scheduler +The scheduler is configured to maintain a target `n` of in flight +messages and will use feedback from `_blockResponseMessage`, +`_statusResponseMessage` and `_peerError` produce an optimal assignment +of scBlockRequestMessage at each `timeCheckEv`. + +``` + +func handleStatusResponse(peerID, height, time) { + schedule.touchPeer(peerID, time) + schedule.setPeerHeight(peerID, height) +} + +func handleBlockResponseMessage(peerID, height, block, time) { + schedule.touchPeer(peerID, time) + schedule.markReceived(peerID, height, size(block)) +} + +func handleNoBlockResponseMessage(peerID, height, time) { + schedule.touchPeer(peerID, time) + // reschedule that block, punish peer... + ... +} + +func handlePeerError(peerID) { + // Remove the peer, reschedule the requests + ... +} + +func handleTimeCheckEv(time) { + // clean peer list + + events = [] + for peerID := range schedule.peersNotTouchedSince(time) { + pending = schedule.pendingFrom(peerID) + schedule.setPeerState(peerID, timedout) + schedule.resetBlocks(pending) + events = append(events, peerTimeout{peerID}) + } + + events = append(events, schedule.popSchedule()) + + return events +} +``` + +## Peer +The Peer Stores per peer state based on messages received by the scheduler. + +```go +type Peer struct { + lastTouched timestamp + lastDownloaded timestamp + pending map[height]struct{} + height height // max height for the peer + state { + pending, // we know the peer but not the height + active, // we know the height + timeout // the peer has timed out + } +} +``` + +## Status + +Work in progress + +## Consequences + +### Positive + +* Test become deterministic +* Simulation becomes a-termporal: no need wait for a wall-time timeout +* Peer Selection can be independently tested/simulated +* Develop a general approach to refactoring reactors + +### Negative + +### Neutral + +### Implementation Path + +* Implement the scheduler, test the scheduler, review the rescheduler +* Implement the processor, test the processor, review the processor +* Implement the demuxer, write integration test, review integration tests + +## References + + +* [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md): The original blockchain reactor re-org proposal +* [Blockchain re-org](https://github.com/tendermint/tendermint/pull/3561): The current blockchain reactor re-org implementation (v1) diff --git a/docs/architecture/img/blockchain-reactor-v1.png b/docs/architecture/img/blockchain-reactor-v1.png new file mode 100644 index 0000000000000000000000000000000000000000..70debcd66988635a180efbf0fde3635458c59597 GIT binary patch literal 124042 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 literal 0 HcmV?d00001 diff --git a/docs/architecture/img/blockchain-reactor-v2.png b/docs/architecture/img/blockchain-reactor-v2.png new file mode 100644 index 0000000000000000000000000000000000000000..086bf71bd88d47e02fc4f47b563ebe365695836a GIT binary patch literal 120617 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

Ah*9Vo97=Fg^|9GhFQ z)I3`#&pdc-g0&OmMA&=XGN^LB9?nR{PgW@ef+?=e*Iv2WhJ^B#Fy;1g7Ra#Wav8Lg z->tM-aV9s7^=QY;;8_|472uwPbz*H}N#91K(iFbRZhqQsM7-3!U|rnMtxzzzt3T%}l%5O}FWoZmM?BZhJIKx(UZ*C|+Kpj%cwF ztCl7P9fxxJU14O%2n8ufb>vI#60MXR@oISCp6!UNLx5;D)YAx&j#W?K<0sTG9(LRi zAl-Y7!>?Jp6Z>#KfJ8Nf;RxX&ClVd8)Vodv5fr!`Z!#RQ1f@WV)!l)X(~45tmd^Jf zHoG1rqaNcV;^p%Hxn~$S&q&pTWaH#`^TZ2=^89^tf2gO)W|^pIGd$SH))3{M1G&D-w39Q(pEAk~Z74+i=FPPC=KB zRm5$B;o4EX8_ApRdQcJh)QaW0SQ1~Jp#R|E>fNiDWqrz3d^JxT5)gPHU}>j-F=1Rb zO#!sF=#>#)BqFgeDJdv>*k^{sn<^)LNx1EkUwu%+sbjNU?SIFFC(~ZjfFFos&2O*` zsEt*G=HoWECv4&jwSf9Dk@IKv?@KM+a7SMRHfj%i@#?T`3spmGddf@`Pf_PvgwYSC z%+T2-{l{~HM+`}e0{(2YFPVQT=kouNZwIhw^5SD57tPOjF4j7%hUKw}93C>PKUoNf zuAGx$<0p2ZD4xPNNU+%&KJLM|5I6_%OIr)MA7Jb=CNn@ts`7#ONeDr*w#Qre z5h6^x4)4DVIKj%ax@5BQsh4wRkPh0*m=l3aTy%pj+7Lk(xn{TP7+8I{UaAlc2mGoK zm0LE5FC8pWV~ag46Cy=AB)5MyTviON84B`+7*R&$lVh-`?Unt}Z;2_Zr zd(yI3&l)1qT@}R%V-I#_ReAPe8jk0!?Upvz*}uAExfs zE{j~-&*PSqX0zh@Xcm%FC%LbTH!bM-V^IsdFX`nUAS~|x^W54Pupwm2FED2#4FcWR zmK!ok31NPGH;;P_gOq_}Uu|7N@^iMhU&#Bq{V}~XhiHvG?6ugCO2YnSI^Cp?(zQNrE$p6)`)B+*2LcFU5k9^k(t zL5)gpd~Hu5E0Xs`IXsd6{N|eRvu@|7hcD}A2-VtdGM{}e4~y>Tp261{`FQ3-Cbm|C zm`T#PAZSn2tXFgkt=B?Dh=CT5YLq=4s*WB{edA5{LC2dZ)kdQgvU`q|^7tGIV=wV$ zuxz#7DXQk_@BLB9R44Ak8aY=1?b|%_YsE_4C$u<)o(v=4(NR|$jYw)D-S+U^lUyUMta-oSpD zIbXBKZf%5-&@)r2p!)w9d+WHWx^G`tQd-z-8tG0!P(VOZy1S7Qq)U+8beE(O64Kq> zsf2VREg>Kv2!g;nH~ReUbKY~#{oH?bv({WQ#~j}nO|^xrJt=98PTkq=R(Pp_lkHho z0%6*$xr3Te#?zr!dVbuS*D5TTv0_#_pVZkJiZ#oToKR?rAw`dw-OdSnE^_Gle&WvU9-srH4B%i&XUlxclra8qBpP^HP zsNcT?9nbFJQ4)6a*BO$?Zyd9F;aHykqS{1FMzrqK3jGFE0x8Yb{DvB^~FtBk0kYxXNQLZ)z%vRq~(Lka3~M zoDkSvc4C_BO_)@uR6lYeY0Az}jWN>clr}Tp6p(87xeE8J4{&7N5ZHZW!=U(%j>uiZ zXm3uIEe?q;tiJGF&X+G=DypjkfYm}tDewB%l%AsS1r2m2zx-%W-rzu+ph8Rg(;HXH zAgxSd@)HF8RUwBfS==r$N`J9O;JQw^^^y0N+~f8D*{7_ltv`pbGckcLg&b8`v`3X+`J?fgfjO7uV2O|ySP$uM>*AM$5;%x{x z#Lc`!zNs%fH`Pc|%12TgFNBG$4ZyNY1NG!6s()90#bj^sjUcUj{FAP3w6g7_gZ{@{ zL;v?TPcVesLC%&#L@3IgCV@eg&MEfEy(_(;Z%lmbjkB>1kjFwE4qC|N^k?aT3@qQR zB>09R5<>}?LO%KYJ`~2#gW}Vm((7*b_6HoG-}8(_3>xk=Q)gsmm2zB;9|Z+X@(Ygi-}~tD5n{FI5OfxjvA8!wTIdEa|NiQHbgfw ze?W23-lk1?_pX-|Tk+t_Kx+94W$Hl6S-f%%%Y*tt9bs+W^62WB3<*w?4erGcK5d^I zfA}Xnro7%Q*Xv{R)f#mKeUrPe(yRj@M=76 z98rM~?}(dpiV>zfdd~E6#+qe~sz4wvzO;vj*94NJiTLk;4>tHdsD}5V~JX%eMpfd#%+pAG$E@HHW zKdJXe`JTrsi*5~VHna_by&0U8-)yck*ve3z_N}Sc`sp@~KJDH}+u?>H3}%FW;}0sm z=rl1AtAzpx`HN;u#~?^4rdXMg=*0B>b@umVQ;i@crN?*G4l@}Lboc9_T}pv(7jjpL zu|cQ?N{F9|usu~;%t)vmUVKWE44qC`!>kbU646S$J^8Ee|FHK|bE4iCbGr8rGU(d-LrL86!H zB3XB8Tm+2uiVw11^5ot7zJPv4fWVfy?F89vuxT6_yL)EWJ!qewDHZZ~6aiNeQ?uZ= z$!ATICK5|u&;G!mAau+GP>09_;3iFlW|693c|)JRLB=^&&y109ImT>Eh_xOVUa?O3 zRw=t7MyL!$q_MyD_w5nM@@16kxQNoreq+vFX*cV(i&~V-)jdKh+9j4W{z2sxlSUmP zi+WKWPa9*av$P2R#j?RFi6~_~fwLcSLW-yv^HcHA`a)b~3@8gtmo>1wlsiGvQG7#> z0UDqsKh^wGPCWsv?L*>E7rb(Hdzq)^9cB1pX*8V4OAAO9m#-I~Hk$KccB4u7Fl66az42R2~yt85I^G zLp^*@)1T5t59Y=^+4w2?`7=1sMhA<~fh`~hGH!E*ZSPJg22sY1oe&XM?zT_`y)ian z;}C8o$5StN%2qonBHUwbst7`-Q!AZvL`{_65&l8!>V0SGJtD7f4zNkJSP@>tXjsnH z)OPkQsG~8}+%QHgZz<6RVdD2vsGu=zaOCb9cwWqk=3a#uwI^I(ZN*T|w!bKLn9H8c zfllmgeWvU}3|fP|(ZUSE$eMNE_@ZFyx0_Ss)%zSxpju>Xr(~_mRZJv3q+F4uFNhm~ z9XED;?NJscb`Yl0kea(mb}PAy;wHm{^O!zZ6^ZCfyx({r5veI*`s|kCB=1o$49Zy^ zoB?T>wulNB(FUK-dIWnO%e1Z+B1RT}joaNmtZzuwEx+*aC>Vxf2=YY>!@cw5fot_{ z{YQsI7jRdtU!J*R{G%y|;%&NwV361`{ zoxb%E!#fpRY8pP3FIc&jusQ-uR_|hy26~KWb>TnKdj14QiOJu7gK=JU8L++%H?&Y*V?XB?VW8>4E!@D^jmA^=eR{L%6R@ zKFT;XQ-XS{vq?smxLu}oi(tjs#`m1Kb=^#2v6kT!M2ynQq@nlpB?2Fp^r$nHg7K0$&YV?sZIuQh5=6 z*S!_W?jU{A0jv{y#PFhyMYha>3_lR4_}+H?{Bnjkk0#|(J7%>PN_N0UmID=n%RT$o z3@6Jwa-6-Ea_J0FebIN7EbsHY7An8@Mp3QIIRP>1JD-bagI9EIJswx6$4|1N;|L+W zFnaG2I#6kOWDL|*r+V-J&Xf_Uv$>&s%)({P_%-9)BvU!9?(W$EmEKSCfw+fmC3`ZN ziW!1IxOw>T(AQg!hVfYq2{#jG-4wk}A8fd%f*7~xg0s!X`}bi+D6hB}dM}znqChPg zmJ;PsmBlc}qOYa*JJx#UrUfj*fpyQB8|q9ti;6=bc@YD|h-Z^(XR2+{=)_P2eh#}r zqz~GbJ@p?aHDTX+kiSlEA-o;!wG~^S=|2K?5Loy zP?R(eyqWk*+HES|kRjC#3exnOd_GaspkWuv@5>YaIg>#SMREZ|dj4Ql$3FJW(&mF) z_JS(MJiJBMPv7^9i~%1AG(2{?7miL^Z;;8j}(lkUQqZ0U2H;csle&lQ85mE8$%^B$nk}hCZ`>M#C86c zH%12W0#Z_u#C}iQ4$#~vZ0QJsJ!N|4vLsMs}n!J2Ru@ zv`Ck^Cv>TN?9^~)oqn{%gPQJAl6r}M2ip(8^Wg(iQqkn1^EFndd4~FU0HT6O!5(LZ zt@pVVDv2yV%7oUl^6;o%p&llJ&!c+>ht8h$KQ0m#!AHa(m?+XW$0AQ|Kj`~$= z&&QAGfLMvJKf_1D36R&5g6}h>dUubB1Rd9q0R~?n#k*c~iw>s1pvXA)1p+CAJgB(2 zRm1mO-@uoC=SUDKGXmjGRLy$rtc^k_@U`_4Nf&SxnTCU46DIq-dXvCYCWw9*lu_eN z<=SFkMFjDKjN91T9p0CdxN95`ZO?~WJunj-<4S>&xSCV&D6=anQ~x(Bn0S?9L-J6LA+7``~x2OE{gXBTttr6dPRL)|6=eR z?r6l>FcS-l;KM_Q^&JU2W;kpOLoZN(t!07=T13F#M&pvgyJ>WBksNRk!~qz1TBxf} z#0~*dDxxBhfYofqooy>H`mc zfDk|>QrYgb`3VKk1m|k=fdiF;gM$bdXLBvLvI+}0WJ?|>`%r|RXbPDJFEISqEjo`C z2BXGc0%)?RghU{jo zeHM5}bX1_j3*CWf(gn!8qWSe7>Djd_?tmg#fz8d$`b9&hlMldT@B0h7ea-Q3)u7Sr@M)rD_I?|f5XuXnIXs= zhAp806hIRd!;vBb#fxqwCF<%46yqB#Rsmpj&Swsp!@m(3jAMWcne%t+{C!o_ov%m5 z>B2l;RX>oo_YEa3j394_83T?2+#nh7Hs~T);0xo$=Zr*+JwhE=B2WM*itH@Z@{gno zM<*1jBDDKZ>-|i#9IJas-lqglI`NL%CUYQX%N6U&{%G?0jy?Wjpdij^BLN+g$!V7z z@ZZo!Q4<1OtlzP0wuD;(Gv6ybrkW*%LFsN< zD@EC7@A9dpBSe|vhQD8T+EE`FpOo-}OoEU#hgJxfjs(a!6oP1a zdU|m73ph|Ftrdkle@Xz4r7>_9Yb|5cQY1yJB2@O$;^+c6@c%Pt8;c?gi- za=UN;-meVz%dZ4^QIrhFY`QH_hGCb0*JE$|uS>!Gglmi<8;&0dj%J>u6|$yfrIvSP5J696f}5IqwmIZR6bO z_hm|`l=Vm&|H~N#A|e3+0UChAr*NBxbA)ekyUL49BZf~*jvX3RlT)xh&UA>&CbqT@ zP@MK2t4zh#on#J4+84+%trymAGRC3l7Ox!lRRX~PFvMzbJf%VXX&1v>y);&%ad+0m zia{c*69?b%;DpP)LB4+s^Xw3KHSyH21^fM6AN^qN$>!|V2QY~WmtoBk!?rsVWe)>c zmA_wiA0DmWJ^7ms-Tfl?8gu16%hXK+u&@oNJZSfPLVWNeuiJV56&HWV!Yh1NvIk~Z zEV^cron2qIzr4Vmf3@p3Svaxnj#j88$Qe7i-lIjrfba!IYSF$cR}7L4$LCYY8IOiM zz~sb^*WJ@RBIgY@oHih<<(KO2>N2*o<5=msB5{8AkW0-{1t0EIsOPQwiPDE z!Dl!4TjFy%U)4Rb-iZZ<1Fb)fKPxFI>P2;Wx$wFI*qw3 zkL9@R2Ht;7p6O2%8NQBu|1~_6F-G!B>RASXZ@!>)(&v~=Jx<(}Z$bO1R~75Zu>km5 z-TBfc%%W9FVlAOjszM|##gBnN|Mk@I=DkE3sVgZ)XW)4IlSf_FM}js_-9^!uI3m19 z3v|eIqBL!Vz06U9S*@YS?vB>brIPArvR7W4N36CpK9@f=Ou1%1los+d%7HanSnIR- z$yvSqqI>jZ&OX0vrKFgc*vi(Kr*D2tnBu7Ot5>L3PTazXRYc0%W<$uATxyb^m)bq? zr)|HPu0{Sj)T>1X#p17Xq+~=2RZAZ=oz~&sJUe6c@6X_CqI@MZ-$5r=w|yz>yF%kaLDK7&T9ibhqDodRYZv$doE?^2Vt+h7jNwKTahh>&jQV(nrPa; zY56~BP?Ab(H~|H8FBJ`?4g_DD^Xk@K9VQZxmz8wlkM^uR`fqrt+0>&~;nlc)_WI4p zcNgsV+<#hRyAwWC2#aMjilvbZ*FL+qyF|ely?1lpoeWpgF=#mA`Q!)w0?lf}>`&t% zsRP-E!{3t8TJZ3!e>6>da#<)kb)=!N8cz*Bqb$0Z5s<#}n(8^fvZkGE5Fry?nJNhG z<194lMHEi`Nvp9tDe!jP@hKZK>8#tRbmV&hrS<)&)U3&Mtz*<}QG^R_sna_sTB3-E zO({8mxa416Uj7BusudI&)vdE(UaNKZ)7b@sOcrRlt<8^2!xq?vPPL=59%*)+21CNA z=%RV-ZF6~0!?PD;&MWr6kZy@((14?O?Q=~B!&sn1G%L|(%;fu$9)u(ui{8`j9Fqkw zF$Bxhy4}z^%_T_{=)=v!wsZ$0JU~7=8dkBZM8E~Ts0q?hXjgQPesB0UEH0rJ3oYTf z3z0t-FOy-&R4>rSe%0L)s)>0ymA=aL1VJmCYs;Q z&~mbc9nKI?WPfS06&K-7NJ4$EtXzM9*0w4}Q??d)HBIo+} zMxz0z?%SmJMk!0jD|t*PpyPP`f&`&Y=!+0CaiKszt^XV*vz*AJ@waP-YI+qR+Dtr5 zJQk>ULB^e$V%&A|t3anduHvg2!5zVqF>iTNs|af?e*lXj=kaqsdwZNHLDEw!J+_8+ zXn=qp&*q@J9R-RSQpkKl^a=O1D9^;`OgJlKIDF z;U@HQ5ymTKeh?#LM~$oe-R~O~yS0`XXncmb9=3QN1g-1Hvu&Iv7F&F<*}{`VN<9>C zPk-SLAX2p*69j1N_Lf3Z;S-`CyNZI~RA)z9mA9Yij<;C9lS$u~oV#w4hZh5fjFuCX zWRFVqP~H#U&1P4ge7io0s?qrsU!dR0cA`K>Py3AIy#I%NRPqyG1s|*uZxP|^3Aw<{ z3HjMKKOB->3Tm~FCV2d2sP%DbFbd~q33+8*9KC1Z2ARj8A*)sbmY0}BL`1+oQeMv> zJ<~~ELBVlnoPOgYZ48nSWRkCq&k(-h{Y991OazmyC`wM2qu6w+O)vXuQv^MzjyHy5 zT&BQGf_jEHJst|rKm`aJa`rKqJ^PHs`a3ug4i{RI2U!Q9sCyU;+ zDodt|m4+Pixwv0_Ag_!UhhU9y-Of1q>hqYmhxLU8H{9=SyIkHIxVt}li0}(fxvqge zS^VZ7%{>7>EioV`Fq(L!wP8?JT>bRGsc8sZ4@Z7 z(7%vEGVgGE$u6O=7toTl3tf$0Tu7mEtq6%G&J2>c=P9DodTuq(LKu={`^QrsmnV#H zefxrhFPLOCMCY>SYN<*D&5^-V{Yj7i@e%sV_M-*JpB*8}@;^54 z*DQu47|DVc8~lNL*XrJ>!*knbYgFkT(u9@oWI_C?Fs1hXN1RDCLtg8i1!ELzX=T$~ zsZKTO_4PHM!-|MWaL&dVJRk-Rj-pdV8uXkIn4_HV=_ajD{J;gvu+=MC(BhZrST1J) zyuj;-eja+ZqLKZ-vRmAl#TxH#woj^0- zP5cKLLnmO-G4ac9pWTkFo&T@M7+t~W%xNC`lLD*?1%UQSlpMMA*9B|^;VOh7Lm}!W z3{~@Jv#t;VRxKo*p+by`{n}X+B1EMw*Rb-?1&K8gKLEo3`v+W;YqUGU+iscmRW>R; zCRUook${MqB*eJqQAa?=e^DQCliqv2>K#OodD&tmJ4o`vK43BLb?;Te&+lKtH3pX| zo}V#SsxS=3OL9yE-H}X{5)c~hvqRSO=F8?#R!ijAjXn1rsIlhNrNCYO7*^-Sqs4P06QY>nUsUC3?U_%$-e(gJplDI_zP! zh;=2UEb1HIkF}QFngu2Y5Wyr+GwN+-r4*(3uGXV|-};OQwUe&*g3nrIB~8$od{ft< zThba8znH6qAcFm2YJZWjcUu4%GcnNEdM|c(h|%#>!#dNm`9cgvH^`?zU)W!sw_Eu6 zmo%3Y{)FMH>iE9I>6E3t0gT?^VeBZfo0}(`T&8N_o&ql0Fpx`u`S6nvAq3zm4+y5m z`{1vjyzx1^H>0SFSVUOw{2~&68lMiQ=KCUkfRw!ddWz^k61!B*$ChjhN7KyFWtlzO zg^D+50t(YK{{^|S#P|H3`@`|!Im^o#`y#OF(V1TVAn5svTs^nS$5obYQAxx2d9XzG zoc(CeDv&R62r#_BULu1_11w`#TbcCQARhNBqy}FZ1fB*J-B}^K((JNdh}*`g+#h>p zGyQK~S1s{qYb{!tf>>jflR#~OA8^`yMtP=~6uvPoVRT7=_^EWIxWSoK`_N7|C!yk5 z<+mwKG^R%RVRckO`3(g*JQFUI_D=opIVy+$7t1wFUOdM^|>ysc_8 zvPOm!hK{JjW}<7Uy7xfCbjN9wKi8R!%alLMG^UTFk+XE!QP3lDBFyKT8@bwUSB_0| zJS>}KRUkV&V$9z#;X^*Sf>hm@Q> zi+;OT^B(;@kJ~j%w5d(9_T~vL#iNCXWB_|zTU-0jAnIT;0o}gHyBaH%T95ndySyZI zNkr1zdpu=TeP~R2i*3GVnrP%^3Zm0Y|3By}LFov~P|!EL?n$AKze83b)`NXgC4R{ z`cA9=M}AyaP?$*!_y?z4WdiNrCzS!Rm-pIzn6EBBQ$CfDCvuvjAw=@5?+2l5APUA* zg#VUm2?vIkY~>S6Hf3-1M9PU;H9)siPkpF>>KDVBIeJq}D#zoKIL!6V9{v6xRNioo zSc~_kMI#51^Qcv)>}1)pgm$I^$rHXL%oqV@eT|CO;rBnVgtis&E83Rn^vJu+ z6QNWwCaL{~$d{DY)P!3gf zG$JrbB0B0$%+)mR94cHFOI*q&u!Mv%W_8I1u{~VFS5{9HKe#7;sk^$Hiz%JVAvAQn zNyNs?p(&5h3s6h8w@>BBpDWEUgQ4?LM-M}{ zPwLy9Ch|mfYm@7=k9>~q@6jye9Br8_`6|OXD;n$CaAa%wCtPsP8$_oyqUhK1Y@*|M zwQoap3Y3i8mEK>NgnoeBfv08L+mt>=$h13jbnZXQ=n}1#mZR2>bA1$QV?AY{ZKjgV zC$#YBVr?>Ubo*T5ok!lThjU)xt8H$|!mkA^T5_+dFQNDE#x3oAp!@~=yCp$#)$#1W zU}NxXp-Pi-d@M%_pmA8EoQ&%Hw)}+T641&}lN`=9(l^`^e)-^R$lkBMV{U`^XB0g=nQ(o&zV z3{&LhkDt7%o(PsMA1P@9S#prK4FM2Dp>v~V*=ta`cW7uxq&+P%0;Ho!_r3%8#S|nL zLDDP|)p@f7F6(-Nz)V>0WYLAtakD3cQ6(p8vy+f%QCg~8AYznG7%wH|N%94r}d)6pS$&-be=WOv0-tJi>H|WfP3MW`VVy3C788#^5-^~4D zBoc@n;j@AotX$N~CLoWq9L+@7+uP$wVSUjHa?gwC(v=N~Pw>myzoJREB5WQNH@2;% zUDHv)We;CfhsN3)bzgo`h+Nu}KlKUS9V9lv_97aDm%v^Zjb<@062hU zLHTmO=KD@5=DPz4Q43@AyLyRUzomugp=YvBkspR+Y@G#e_ zJ?&E#&LfQtZ%|(2?mvALe9;7o^MZmN-0vRSmt~Rjll{TA9=ITe;M^AjJK@oDImsDkxxbwI93u24K}ojx`KYVgT1*m585~ z@L$kn;*wSYV`6hmP(dlvSrtea`|_3E!{^s+w`^O#niOkJ!O#k!zOmsvzD%|0RpRxzbqOfY^tG)n zfCi4L!BJGJeuvsy6iQyi9vwLG8X?;WIj#jNegh@L^`LCpxB%K6vl41>*oVW4%`H)N zJfK_ZzBR6bruV={nS$a^2LTjWk;=3$(*1A+!3os4IZWksm)e~yJX-C(opV%RT{@mr zkLZ0w3?FHRvBFi86maNEGJ^2@;>m5q0#>6`(vPT z3vn|1-7Qi7K5hUXsfey2m??Pv*)C@s#%b$yO4F*8 zUi|on3-F6XjDZc1ZaAIDz$-?1ZioGYjHz-zw{_EJ{-=M|>?jt3z=%6gWP{4}A1t`# zB{(*onc3R1FNX1v+(nqfWGWUSGO4~{@wzxc7qUUkT3%7f-umykNNU@87b?sIK<_bF z0P-j-3NAblYOiY^@(!g~#*Bix0j;l)_(lvvBj0jI2;MAJ;tejQAJVg6>`58Es!OV~>;vD5{^*9@O7mg$vT0{Xn^xU`;rGX(iZHvNO)*Ib_x$^$Rm z>`lTJY}5V)tU=aZcH>j~-ugj(}pAE#fNt|0Fe`M+H z=s-4ocdA$`Bt1QSBo>SK!5y$RQNVVM0}sZfl`Fn4qkQ>xBe}hPmzcleuGeEqks=h;f^sr!0b zzf>hyom`bZ(ajOZmvh;hCrfWS>%JD!t8_R}C+E8#8FKF+8zcpow6eG_?vX?RO!T8g zLKTcAwQMaMEci@G_&J34hzOi*aOF3uWXHk+8CWRY;AnQ8E(5SLwdc=0;wP_4Ms_DgwY6v^mscW>aTs^*KcZX2(*}7hB^V8+AS{i+TMNo!7+|XisT0G3l#fvz!!$k2{NA zyQa1NL|QIS%ZDdrgt7L*N4m(a43ph=F+QArlb=AO<~fXh37Y4FEhA zppk!z4m9zww(?cL5nxwb|M>oCJKg?`ULL$C76`q%v2PG)~ME-ga{0(3cpomBX&J{rxjd zHz2W50MJ#eVJLPGhrY_>*Wva3DvQA^6$JW?3j(0-(m{I|KH1=b&h}#t)rsE(p;yF$ z;&A|+@o@+6H3TGAfE^%ecK`}BU`N(2R35DYMiU?X#)|jUKm_nfuz_D@X*E?DFjnXQ zGz(l@T&f9*uP+%BK+~a4jo@~S>P%RIt4X&pqL!m5u3lLZWT>f7;WOii7tvGpLQh*~ zL8AGv2b-oC_KJTy&Efr=_g)oW+>4p%Q|%1_q`M=59pKJ`momNJ8xxwyZGDX9b@h_~ z=)zbClwVF;(S3@13j~?$C3J`&>c7IPSao!W*h2fww{JhECr01u-*>85)!%P6 zKB`fvS;Qk}<*iiyO+w?Eo8&QO-3ww>z>9#Oe|nfj6z{3*K5GhPvf+p}O7a{(3n^dx z<#YuCkXS|#VQtLW>x1?v0V{yRqkt)OQDo_TYvOagw*se~-S*3}>$m6g6+?>F^7BZOSEE!?!qn5Vg?E zWUPBUM7W~PEJyqlHLBFrv<4Uz+6M=LQyB+nEfmqI)H83O6P&^bxni3yE65%xy5Q)9 zu{k(Nbm(%X-Fr>wy_?725sd;>P@dGMa7n04PruKUSyG4JYm^la6ZSgK+|O@dz)&J&0`=9(Fa^D_x-VQcSu+18`ik^6bU2Lm z?#jrp@nq2qad*F>I7n*Ysfjo`I{|xJ{JcgOn=;VOsUPg8J|4{zl|)|h2?M^dtN&vJ z0{rLhWwzk#%>@quo0JFM!Y`+7>E;`3R>{vx4H>#nT&fNt46i}}n;%3L5z2^Lij?AW z7>QiIJGv^ULAXGPM$D1+Bq+OA?)kX>k(=}?s%!W%_ZN{*9|Dq_6bN^@Kz%x9m4cxsz|=G}JRA{3 z9$sLeiG3))Nn&mu9!_B@>!X7WB{CdNm#cyCd@y|*^-{#i)LceH7DzsiHCU1iLTT6Y zN4n?Yr7GO7*~r_^?`-&w|Mv)`^oa`~Vy3IV1+0=5?<)+TVk7jWd-_$HZ-VIDYfvqgBMH^D*M_N&A&UOC;rng5Bf1dO^mHM_oE&ePA4(e;GRBb3CH{ zX|oLb<;(yVbl?UD&uWJuu_7wn_X`=sTJ2IRvXPXhunl>Y?t#%6p!oTNI2-B{dFWbr z$e;^(tAkRgyelg~XN5{MlU^l;#gpg`;DiZWl=6Mrd``LN=|it?*8A^e3u6#byv!-O zkmGmQtO!0kJw-_muC`kexb1sTo(R;5qySv%qT8JT5=x(GS#QZPJd4Ignx%82X1djLsD@4)l^iQxQ#wpTcM_)u-Pp zQYEr{6cuR%S1sTXpbcDm)7y@*);`jq_WR zy|Yg!|7KenqI~?7HB_tI(0C>;0`!c&zBnRA*kcT?V0{yYsG8pDd^|B8$8=u2HdZ2P z@8HO>*KeAKSAV#{BjCvD`p(L3Pn9|^`V$J{n56xt&wfd6sXaw^8xJJ{L!Q6; zVRCdfm4S)pwGYvCs|hxUdGz4p8}^Hf7|JiEd4t?QB2U4fQ{DM|(dEeg-uInw|S z!NSk{iSL2*0i2iaW+2pxRYz2X2N+^p65Qt3S4Xk>*3!uCfb{S< zp!|7Rf6=mk$JG;eP;BP-E9bcs7WBfc>QUix13in*bU2|h>e&tq>?ck`qE#Jk@m79kKW zDB(kw&%hI4)$YgcrZb?vDQJiN^#thfdr0j-E~MOW>9Repmmun>;IuIm1@3h{0zp#L znSiu`yl_zJ$gyGNGCKdb+z4Z=@3SYmU66jvSIHyOyhsvq_RJQKBXYHLqA;gEoNoao zbI#$j96(A0iXa!2Jk^<(PTZi%R9^V8IT-Tr#mIIKM&Qp7P+qd4rY50?jL%4j2Huwf z>%iR(RKz8XrhJ+KYd%-~GbfD^Kf%4pXhGoW_W?2*4C5YjxvM}9!{jl?# zlz^U%38Nr0(wSnAxK!GSQsHD;-y3Euqy%75ih@SoGj4|89VCY*#*I&}F>{Vi5r`Ca!GB#0a zLN^zWoREH3(F7n%Y>#dQ?=gP_?<&z6kI8 zdAmSTX|!VX`^u*@tW>b1(YKB4?d{Wqy-ABTuQccRx!~b4h`XdQ;5iD_57N(-^D*di zeSaA;WMU)E<0|j+ScVUTGP;vqBZE3oInBeyV>N~A!J)HsX1yvvmc(}aCGdq(cMwWA z%~+U#ZYK5VW;+dvufxCnY?^eI59OSW+;)-@~d~5 z$m0UjEjn`Y-AAeIrK1a5*Ppn5Of zF2i59JYz%|dWVexywT)hP&J8xm$kL=JqomS2cs-B4!*l)skz%pdWoOM_jh>*1qP;s zNyLIEU84D%4tfddq#kZ}8hlWUf2JMWBX1UUq^JHs!Lp8}oKa=-k33ieu)K51bJRR2 zp0>nv5%%7i_MAyQ3#68cUR-lGcZKwNy>_|b7&;~h#bGFJh*^vVN=8QQKKUL${cvff zFwVlFc7apbAzmjuWsqJD0{9=Gch7RYhD>}7{9LyQ>jxMM!eBFljP}E6*zpVs zAhmlNvV&NZE)h&F8Tx@UaR16=M+lN3V?<~~V**l**=DzL#b|DZVxn;0@pfO|b#l6A z(=XwVSp{{kdx2-=|MD2BT-Y&iC35!|>a7@4Bd&>q#74lJK!`5?jz@-^v1ET^Wd#Nt zlTFVLm-`Kzxotq;MwjUu0cG|V55WPg{m+>w(hIa2V2VOrwSZ5GH#9(BgqW6=e-IaOU+IKvX z$VvoI1)Pg1i+4|bg=uJLR(5xbhD{pTZi9b?L!eB-k4xN1#{ZM+et@8W3o5^Db zxC7;_@`Xnfe3pXphTuOVdI!FHG&ru0HaMD=u4&i4Bv5=Wj14xA(xkzlonY`GFP)6q zeS|navuAFS8lySGz-Uq~mBn`uBhD9102wc+9%Ev{W0uez@B0;N-NX{Z==(thoy` zDc&1QpD}=To$OA@4L>>O{^tNY!UrCNQfU7S;4tnX2!+E)goTCQeN-OS2QtJknRtfP zo-q8EUpkXNzJ>SlPeX8R7NwDGeziZ@62SRa48_oah!OL5^EO7Dr>X#WSBYUODaf(Cjw{`6IYfR7vi<~D#r;g2FVt6U_&6yfdE)71^fcQ4W`E&6)ji{$U8_?_P#&3|Q7DKHDP z?N5-6z9_Qo@mx$WtIOU7J8*a?z#q^I3^uuEE1o0CCUcbj>k(i&xMSVUMKFj!t7U*r z;&!ko3MP-kw`q1;p7_C0F8Gw$yW{NaOfKlIoYvDF_Gj_}dr6?XLXoX9`-lZ3Jqjle zw{9&#jSUXtbbQ+3hWUYxD1H8W48F-7?Sl$CIdMd zCaB5@0h`av%SQbayW2T^1`5Wn4yQhrj%SGa_h)L0ta9C+|6u13AwvcfZA5&0dHNA8b^!SSnL+DZ-!D<-lerbIHUeo+2_(-!%+>%kNiME$2}mH{0gy#_BE z0^bYa?GYv9eEs8VFIZy#XBw{9SzAW{EoRr;90@>Nh`K(WeP3Q(UG)RDl{rA#1oT_u z3gwQ4e+Gj}50Pt`6X?ZaqW#tm4(xEh6Gi*cEM$EoxJBXyqD6+>idtJ+XREJ%|H7a5 z%J%_y38=J%*of=+XDi+|RfRA+*nnh|=)n`SH5^QkxKU;@m>my8~%tlZiuhhET80_0WqdZj z@3mJXDQ~#x`-090&cDAsMI&Cj3_^jPUSyR2h4Y3Xqnl57;Tne%%G)o7@`5i`P*YQr zc=pU`51-fli2l!LXHp=Dd$ohd8!rFQ`1?;fh)rJk$IVLB-i4nVt<*)((a<`8s?_md zkssV$D=AYlehS_nPUAEZ*4EX{2F}LY^R>kp(+S15Aj>GbpI*IP=jYG&;0U>cLj|8x zO1uwbAdUYaSkOU$Y>@>h5ln_H4L+I))_?wl3*k%kP6N^;TgI8qOaJj>b5cCz>#H-P z*pKSKMb{VvL;$YI0p%Jq6}LY4A~SW2fq4wSVV_@Ku7mBfi?kBQNGQ8U%ltLya zC$I20eK|WnpA8@`CZje%uvum2%sW*2(jI;tCev?pLIH~;I60Zf#>S??VYRD$sqf|% zCwoAO*zzJ?OY295!qZDu06>7OufBmnrTdYI%l4?~Q@xM1;mI9<^T6e@tsF@qL<%ZP zn(5y5T@C{-1w}NdHVMb6Qy$w>L@;oZW7FlqZK6Rq(GB2jV+#w0ZcT^8+xhyAw!`z$ zaOI?iV#|vOf?u}1kV}Z z`y`ASx4^Jf01oGTjAtYhbB=7nhI`Qh8HfWYM4j`sBML0ojt2C@jA4h1_h&Xu-v@y> z_3U6tJPbmQOH3RJhGVCVUO#{i9}NRTtoG%U!;h~DDXFQ9{7*-2H$1Axh#Pf3b|x5C z5xYJAfP(EEiqn>?b zoL3fp^t&H#pxaaM$R<7nXQ>DdGLx|lxVRr}kI}wbh!YPf)RA)9V%Dyp8hQWpv!D5t zSzj`U0*(2Q-9KCKd6QJrgRU`XXOI=iDt87}&!M5ABGKghEuiv+O1*4NjM%>DRmVg&9?U^`!v!tb0nR`ZtZHdnABl)4%XpLS*T z`1F~|$XfuLSXt=|Xe#DUW+U+fN2zH~D4rKfj9DPRDd_K$xI4YVj;~%lTWWGWt@O~( zr^Ds*`OViCL;1N<`AtWxJ=nTLd_bca+J)t$qh5bb!qt7({73~6L(&lk6B7xfIt3A3 zAWC4*KbN5B{-$dOpQ=Z=RD-gwmbr$gvmMr{zsddy6ilhXwCFyt0#8>scE%rCimEE^4r1cCom>dcnDvN<&ZU%sy45u;-gny{-*SNfk59|*?Pq3jt0@k zN5c^{q(DzHetv{P=;r38Z6@yEX0(V7F>{3YH=~N7As}{AcN*MmX?_{lzh{y+BKGN=xvYZnXz4+JM9 z_y&@VLlWHGg9LXC3GVI$cZWdm;O-DCcz__m-Q5BNx9Oea{k}VQ>ekf#F;(;9R-HOk z=fK`{ukK#G*7H1THJ(R3QKL#rSO6O)kvKe61~!~!IV-$bl)(pGKSJLhC+E+2g;O19Sx8(_tL#{_JE!eD?F_0PU2%}hcQ^tizQn?%8&tfD9KNM}^Z z4t@)a1b)plTkn7W{-oT7N&x;D_7ajNWjPW}!WnB21t@|dhbzNg`m|t{?@$nQHjc>6 z`+uhK|DV`STIt_{(ri$I4e|nhPS4Eh2fNk6 z67bDI_;d+Z>Fl`syRtLIp3} zXhZyB@tMP;XJpU8*gwE8XpNGAwgI}Hm(&4|NkTxshpz+sSEsuYDQpZdQ5u+%xccD5 z(dr0zU|?^dS##Wi>2jB6Q}(*iT%O;3I1e!nrmFxPPrhibuHphh)d=JM z1RCshAhEMdW+Nwmz<+=9%5uK(OP@`(wxx36a)*Hq?*l>zru6H>D8F+Io9N1TvwI0O2Xv{4yAbNtOko4p?SyJ`4131|Zo$1evwS z`}a_AX9qa$&t&R%1Qe2h)Em|j1N4u|jC*^TDe>ubL8)OlQ+DZvIwx4>Hwbg{dZFU0 zph|x{u@1CfTLB#_w?M}g)3JOX+!t^1yYxVlqYB$~Dey?V)(^U~4H7|eJ2@~&7(GF4 zUTr!53=s{_yB;}IwFf-ca0E1L)Nguu@OWMA*Ow_EcbNP)-a~xXGCwrGNX09PwBFyj zv0~mH_cImpT$X@>C`x+xzHILZV*fUBsem~JB$k=g`S-$-*F8!nCkNO z&qAFTPd$$>z{1|NSK<-{l%Ebz1Kv=OqP~^^$j~Vd(#TOPue|s#@H|Qb}fc&Kg@r zzci)*ierzO2XU7YA|C3$gQxVY7GgMsHv+($X0!^PAdCsZeesx4x22eeLnH`fnL(ed z))tM@uoXmi;{D6$--`}J`yU>*-baAq;-%F{{s4E0C?3$}MPA(A=uI^}z3~w({5{B| z?FdrSX&hZC#c-x+Hr5DMw(@-Zi3xRR)Gp;T%tu}@pHxciONjcSkps!~RU*7Nu!-K2Q7022^wiQJ3AQ4ax zrhu6CBI_hS5kYw(DBOuVExr#LxBNgo)=e%G5x_1JPNp+DxCSqaumE(B^~Lwp7i6hl zv@SCk*89Y_!M4T%r==U%bG!#}8krZo&4~&Aub39H;ty%}55hXGk-gLA;m`WyWW+L^D@z16 zvABzQud2WiIkt~jUyJK0?O+;j5kaA@`w`;Z-L=(ejoikoBjV`ABXE1Hg2y&YE9eXY z=ry|RL|Pv7*+F)Kf^ep%ttJz?Bl0@t^6dscCm)r8w9#!`S-Jt8Ni0~gTE2}Ja3FGNMy+qm zO>B-0!UcsR`6?yPz!3}(3C1Z{+yc#LpmMI$W|Am=pm7xKq0rIZ6FAXhP#B~YO#AX4 zpm&cd6Jt2kj`h_)Voz7Gz8@Aqr8;KO;Crcw@T_0wyE>e4R|vl2xw0)6g+VcsSJI|d zP1(Zg&&l0$A~;bV_O<>wpsA0S6&Zgq|WdJ5cf?EIe zTP+II>*gpBhlx?EKI0<}_DRa?4_OFta1W945estiH%N|h<3!g0oEhH}U=br)OiUJ@ zv_=?)^1Bixew7JaiOQtJwy(t_3PZ>Zj;v(>HgwD!jaV!y5bYwq!A0W+-csrFJLw_w zQd*#cGzFOi(-%?)f2~Ckv~803=%ooK+QhnE^d*Qb0tFf)9&soUgIMqUD=5@S={8Q_ z*Wh!f{atg90q`mjI5oK7)G+A;K%9e+gQ>9Zjrplhe4G<|kBBZm*U2VO@~niD5fhuw zhf20Y@?G_Ec}?+Al0}LdC0@SagYRAVQ5jhEA|oh>^29&{6YoQ8vq${g3@v;}>kqrq z8P!9Z?cLMsaBs*XVVlGRrni;9@Dks-NUq(~2NAFM7!W<|sw|K1S^>H4So_a~6I)L? zJSBDWkPzCD^DI+@GF8hFrnr|C?&* z0BIJI(W}agO>ATtO$f2I7u}%5mZujN{F1KlgZtQD?&A~AGa8+Umw`bS6guQM?#c+! zpShic2>cVMj;g3sf}In3d8i@H^b%dG3QbBPdeL3w1CcA!5bTn;(kusH7 zFbd^QOHM{AQkDcFY$LwdRkoFP7M$HxPKP@9PBH|8wyC`eaH3kZUu3Zn&I^VPY!>XVih(?e#BWcizI*rcVN-~7Z-&iMR zHM>^Oh(Ra_oTTbpP=-u9=HslO5fenAm`h9+?;^c1K*H^qkM#&lT;UC83#}JHq(t&{ zr=7yb^C0+;0W)w5#Y~cIa;ol|#T{)I?f(M1UTFkCGevr$D`L+>*XysFJ1**-!TA&>=W7FX_37VL*+ab{mL|4?nk)Gd^s&7WzeJvA^iBg0yHmjA z0Z_ zU*dGPtW`yKpNcP-xF_21)rvC2plRJzPu;msl^dJaNT2Q`!<{2a&-LeX^{_qzwGFXk z6Qd(qvWFr~i7!Qig6qqxI}u}3@H%-lHw1(84AhxJ%OUL%^zW%X6XR&eXp9u-TtreU zP@p(G@&rSWt=^_5z#;L%P6Xv{UXWBN71;bzpWOoxK(Ylk&(0^E_Q$>0B-re0LE-Kk z1d&q7>>^7~1a(%jB%-W2ODV63gIPYv31GwNyn*l4qFxBY@sjRKaFN=05VQ=Z&LC<9gj;#Ab!8ELxHX^GB8e8GRKwKM4&-! zNwok}V#o!Xe@v?O!-RgmWlzC+qt~8XX5)bL~DOt^gek@)f+$4I4# zv+G)c&r}>wevPsgIG@%*nBN&<*~WCZuJ*=BhND6X3Bs<-w_7KaQ5p)Jp=EM6GAR>v zh__p{eK$KzWiq~A+(5h<&E(+v0&HSU?^BpD1^*e7Xo3o*_@=hsYcU?&r)!IyZXyJs z*qg}eO?}wJM_d)WKOra4)q)#%mq~glBGu8(`Y*+~QjD-c6oHIrlaE4R7znZyD&-PX%gYE_XyCFB92asPr z2~ss3pF27m-4?WyRDzw)JESf+Xz9Xo2A-x>ZSb`(Hdh zudWx&;mPxe{*3Hf?%xvcHc|!WiuidOMr}vF`%B9G-Pm`A*}zZFk+-on6a8{uQUJIH zrNaLX;v{IW~S>?J~sytQL-9?N@@W=7%SJ{j3wq90?4362)5fkaet4N#uIR z_cMG5-&D9ZovxVSY+wm+y~6(&W_dU`KnxBf)shK9y~)WdKX@<+-4yWk5>+Mx zNj{*A(hwxBF!@Leul~LzHU~($;U7L6sWDT1ZM860QhC9Q3XFX55-!+ZvBllx_x|i; zB5;@w317d*nkds9ntB2F7yQuw!S zodg`RO90&y2Qq-+5>57nrq$QHe6W#OA;1!vLggX|cwmx{#Q?!-{Yda(f5m6eWn>97 z##d{wr3P;;?fW(B#R3;8(Pjsm3bAyv)JmEx)A91IgQh_A3RSyZnND*QkBcn;K#=_( zJrM&;2{xq|y5aAb5)|vVE6}AXPAlu-nHPmyf_yA_R@B+;CJo)Qe%5(FB;n4&2 zNEB$o27`7Z9$5QskcwHa_u&VPB!`I$d@KR#4hZO<38RDnh7&d^Q5yu?-RJ@W@ckh1 z!Q#L9^$^ z!LzGDMC^>91%MxL0;}7jc{{Ef0JTf0ke?&-f<+D92r}Y-M{L#z_v0}LhN8S95IB0a zP!@BP-wFUYihw}__%1}4^Wt*c3p!tL!vso{E`af36BCC4cR>j}L{3M*{9n^q$Nw|^ zxXrgs@b%!I)E8hbF7MpiVWuZuGr^W89vC$LJ5Q=5EVw_5TPE!1o&5Lrd4#}Hi8EJ5 zK~dxdY^&=U68*MRNs z5A#rxEIe5>y*@ufACQ5qB-IiDu$1NUYR6-&4DXc(RA|u0LjU#Ks7V$TCbf55yyO>u zmNUw`9~h%PECQ2D!W%v$JLB$&$i%n30kJ<{B>?{zn|an?$o$sfJr?ud&F3u++ngw3 z1UQIr;Hw{F{_l7MLh=!freZ2$KxuOMK9h4Y_vl5H~&RhB=k3@{_}s3!Rp@Wi&LIMQoR*VLm!J-(#>uJBB+pldVmJ?e6Qvh z48B_*{+;Gta!KoBOoVq-e=U^)ELDeH>Foq|MLR_n9k4*-ipe_IF-YWp4Q1v{(f$07 z|M>vy#1dZMunR@RgP}ka0&xDTurco;4Af4&()9Yl*D?)jON{gDIT+4Dj>TXjG$U#B zAYhvn5w9c37Xx_e;b8A+0=IX&Wp6q{#zD*NC~s*5$5idW&n6vOt_he8;u#VNXwXm8 z!_7h$)f))*xbixns1_i!0lQ1LZs*tx8up!XSaK&3Rq%#j%m(P(<@jKUFp z!d0ZT-Z`8M*^`i6)w0!Z&Bzy2CokSl7a*c}X%oVZ{mOKxLY1IwXVB9Q#;4*ZqW)}N z8}QDTE7Vz{k-3#!x8#hr8<%(#9l}wxvb2ZlU4E?Nw3n-|IU%lB~MO-x$B4jtE%t8n4V1?r5CjGrTTTu$=MLFKo2s0ITTzo5HJ8vl3v zmH^k>_Xh@qT>2K*4W*ztmfX$_+-Ov>L6kjcMaWBdxv`ncUs1 z%As#@VzHOb3+Rmt6l+!ERH+UK-F!ScGCnJdV;4EonpW01a*pz1(yl=FokcXfPT22F zctu3u&(Qf}mz~A&ms;<0ez(EB_wzOQw)gDpGi>jlFzVihU=*)QCyz@{wE5PYU~k@X zlut4p2~^pva4xin|B zDgPQgzVdiCw9aZFL-w{dU#%Jbo$RM@&?i1~GD8oKUQs^2#_3uI{g7RVg+?vsflz=y zxnw1kT8%ya!9qizoynm7;-E)w+AE>}xu=;CY@#Z1sT>s)J?S6sqaTV#v6wyo{fsaS zJ^9r~B3x`lJhxMT1>{7vTHS|38bSyI~$?XAXzQE?<=Ux+5(7>_gwze0$^6xh0WT7NQ0B9qDu_rt8F?aN6b zv(bv)-1Rl7tJ9tz=i<<}yau}rv4Us4S07Zy%+=naVx)Ai8IInC@1V$K_k~+;NAfF_ z+H&I2Dq^`jT2mGZ%%Str5#4tGQeirK-kZw%>dIk1aO}g|Y_X0{Q{|?9v?kv^UEcR4 zzEWn~yL4dZllh%VyEz)i8UG{{g*p0^&ZGM`7M$o+^lp{bPV%ExU4i?9TZJZjo9cs^ zXFZrC;@90I!&V~vMgyO)zghSxv`D#(nQ1&Rew5Ia^wjW|U=t>D{-j=9sXUsoUtg9$ zf-eeG6@LE8q32Hfn#JL_(v9`{_+?=%nTAoks%$cZY}k`!T0PZWZ&?i6(Pc<@ zF>b{?vqhr#NUpP__WRxsX9?zA6C}_gNSOl7omWBNh~&`>zC0D9WSz=9i{Xd-<2$Tn z!A>7pGj_wSc6_x)o8X=Gd2S~KdO4fDdkZqI*W3AQ-IdV^R4G>(p9G<5)mT!VI*KIU zqcNI0xlnsGf6`{~xLu*AOjx#G9Zs%peKQPt`U>e;jb)h1XHCe;2r)KtXFoNF#AqF* zQd});_J*?W`(F4;8o~E{H+(_4pXfXsefIJ53nyqKsia^R zE!`p6>?&6+d`R4fvOBxh&#El)=CRJ*=%oA-)03ounYe*77CnKId`cdPY#JgAXwa4u z;cp4x;(8APNMGOStD^Z{{qKJwEEjk&T=`P%h5%Rm9mClQvitFA*8T~o)vbPPKmq1u z;2E(Z%d7R*!Yfq43bl$)t1&I_%R;4j)$2PO;2E!J)!xOGEsYZ|vZT4cH-VyyihLFPz{8UDzGiCj1V{bK!s=@3tz zDnR_8+NIb=39_3=T!vj?IFnVdBN3HDG-)LOnhdGXl!?U7tnWa};?yec8hE!(ciAO= z>BCba_?M^!+6Z?Gc7|078V~215m9I0^{JrrFp^Z!X3uLl3NQGkuMT|Ijpk8CtKOgJ zp1B=*<(|o7AtKZ0vY*R^L?t=(nafl0mP15kC|P$fL$;Ys;zD1@Og)l}SFknuR=z+? zba!zY4GJE@F1u^JZ_GAmHRr5W=Oxm%6uU}iru3@q$vhn91G+g*x99d2BN#;I7APiG z;0m268gvB_RhZP@WilJH>2ZVw(*i~a?&<;+w^8?#C9U#bM3hej)!W4h!4Y9;^uKHM za72s?#3ZGW!)k~Ye7m6j$L=rsq(0G#RYZQkhM61Cb5qQyW-|UcqEOS=kn!3jv66*8PV$VL6 z!yW$Z6Iq46-2G6R1EZgb=d0aap_@a><D`^nu~Cd2J?|H*nrYwqR%n#ID^<(E z;reYJX$GR2x#UF_y@3bg>nW2~nHt|s`-BZQI+N=iR@tu5w@KazTskVi!z zf`yTaMb1femUGpv8B+(NIjL~G$oc)uSCI!~#!V7#N4%3IT6u5uwE$N+|7NDj3-cUq zmc|D@ zDtBteA%x({Q+f7GrpLEsR6IN~K4Pjczk9i^4@o1keOJ=1iqC9$#$l#~esrAww&n3V z#*|><20L=OnR~sSq1mIQyN6@6nvb~U_LbVbiG`z+AUh~SDeM#O!LiT9z7I)fYPOd4 z5s8UJ1$Vpv%@BRt#3srL9STKgGw5~8iFZ{>RTr2c=0V5}kqfq7!Z=-^svNLRg zUjz2zQK*;_e;;mr%iiLyE~Blw&$m)2(P2&H@~uWHg^!y#G;ZWb@MXa+Ean(jFz*V zNcy1S{obmc%uVk!$5{&TW@v`<&8@Ky@Lrw*e~-y7vT&R>i0GKH;jB=hi}sh#f7U11 zRtgI72C;h$4%r&0^)k=hUYFv?i>O6QOZp=JR&rx*7|XE^F1w@7=xvebGwr`C9$H%1 zS#4={j>d8PrsF>3GP+UVQA&(hI9p6wX*5(kEdj#Vry7w@h`%6*IEzT2;~X&tKYf)O zGD!jd?V~=f61f_uCnfuD^z@hLE+?BcbQwV$6hfVCOFj}Q2b4K7pK7$L5#f|7x=EwS*-&}PN8k2#;H6Yxk2_Mp2pP)Hm@eSN{EGFyb zh#o&GYY6%6n`U~xi;_sjJXc9{lh~Q1K>)Z~HK5fx@~OXzU#)xz(eE6?rhV-T*2k8% zO~+mC658S@N~m&IE_4DX04HkKNm@kSM+nW@)8_F~KwOR{&#IN=L9^6u{jw4jRKFq- zV@Qp8z}wc7?tW9Y2a<@M^e2o3YDBdJ+UAhq3@eZI$-VTzVf^asATgX}|MX`;YvR>9LE76%xx%)F_Te*$@U;ey zlUf|DpC%Gz5z?G7kO@NBkvlkOf-!rdU1mB6c&PrvZq`hAP%VMm6!#2%+od37 zfy<`0&HU-|fGhTfo6bt{`P)&;uQty>yl=(cvYAlMa6i|_)!4cbkn5erX%MN5^~HOo zLPJI)s>Its5#ex;4M1cN10)bv^S}5Pl$|X1AAmdO;whXJ3iP;QTt`bf*va;6a*@TT zT~AcyW5V71H67d+kS{>-)fDHCX(btvu$esj8F;V_Gt^`#7f#PsMEh7sW@qKrpGvzA zU5;+P6*#Io+7CVeo)#WJ0TH5<6vx-agD*mJy;uHGQ9Kg=j`X+8_AD5- zAYcc-fF<8q)CqzQyLA$?*npa zF2R8iACYjR`Y($#(P7S$v>%B1rVuPk&6uwLZ8Va9Dlpg zCWR{MDjh-gyUugGvlcmC+y20l4>Kphtqz9|K@A&V1c8@``rt-zg0wWpbt+X+Z_*!4S z;0AK63<@8gT@6z;uF!}OZzhzVsdAhFe<1F91i<7O5`lEe+Ubi5K*SVReTV)cTzanX zkPM~UEWd3|XCiMEafmTX^ND#<-Pew*2YBBV;Y6ztVh~{v29SLt;+17SRt5QJ zn9;w_0O7qGn^$cq%as@a<+%R~I{P=g|NIT`7+!Cs7A)t4@eKT$lY}1G;6*Z$i+NRjrNGx28ms+oMXB-hyB3dywEYzcb8L<*XJg@RrX6qfGw%NQwBB)@>ni*L35LCNF5`}v$umpE zG3Y<}LAH4(FjV_ZiboP8H;g*^igpZL*P{Bh5I-CWprU_(E;YtW@a%92oBZJe$mv2d_ z!wa2YP+S6i|NYr8W+0$bc+zVt2x!@8x$NS6PodN7=`RwDn+bC>I)#LVlVz4p*8Hv) zD8`TCL_^;&D73op#vdNedSzSsyXI>Zz0`0tU9Neu3TT%4>vQcKiFo*Hr~RHJAH#C< z{+EHfdEP6`w0{9aG$`>y9ZKL1%<|R2E{y>D1Q`}MROJnP%SsLb6GT002gZ znKSWOQ(q!Iv)=WRc89enRIG%kpq4x4@cdOPrTq;$0_IlmRs6Gj&+kcud@f1Cj=x@4zsc=^gL{SjE5N(b=ErjxQWn(MCo(Eo!NmJC zPt|Z_XW727YqtpQR2L{@>&u#})xZJtrD*jP4lX6Rm1Ony3=$`Qf3QA9M_i!yI?7Po z$lKvUJ88s&tN7gu%e$B8DX*0*eldtUUxn`lW*EfBvkTWWDpwfasyGxI-KIGd-h_zK z>W@`NUtGjSj?}$Hk&3&0)^M_+0NEhZ{$-!L2On;o+s<<;3 zLY??#?q`KUAsVGZ1xb>TOZr8Vz6BiI^?te5_M7n&7OyLJ>ZadMe{w4}fd-@%X03}u zslt9zrwVzJB-EC{w`*JBD6A}oJwmfr7B)-gx7N?oiI>5PEEp4IGZ`Z3+tRr-k$B=b zh}CQ0J^V!K5$HvVT~T;{T;jwfA zbL|K}x5tG)9DZ+&#Pndho8b-#o0AT(5cN}Q=4EcBaceltF zr<?<|@dW>goBK2zRNQXyr~r<>59_BECw#C1DAWDy8f z`~1mKQAd!(ms{j&f`s3GuJpF^P03De`-1K{^g3uWxbu57rB+PK-JY4uCw^3>v{rkL zbZR8D$X`i&;!*QJJ}?Zs%jSnuat+?1LhtNneOtriffu`L54*Sx zxP26la95&JC;9qjzticY<*lI67ryD6A#K)4(@$)tU}+m3+_C-Qfdt8ZGer zYJ8W&X3eC@_3~%w?HY=_#3t6qeT|~D_)1W#5&Wi^yO2NerU_3hFrpoZZb=5k;krh% zQ`i4M6=NWZu>Sa7woM6Ms(?w#aN;-HDqiR3k8s75urvVpNXsrGELbJt3zBRY)API6^Ou2`?x+F(Tjj#R@gVg(`UX zXRDhRP<0byd{5-$Pbk(6^WZ)&2>00Ut{Ev%H!+`S9YGW5BtBA{YPMkakeo#$HkIYPBFU$1SAgOn`lbhKEl${gd$iOk!>J`|c^w&3&W=)y^M2A3lZ;2vQ3I&vTp4NMx<=fAj*D;LPgd29u*UASIn5kyrhgQT z4UPE%P=!kq?G7U?_*{JX>P8De8Iojg18#)Ix}AvEWO+57U?I1@hDtiGob<3dt{tGzW=U`JCcV)%zK!bqjUk)kQN3mQ2&Ryr1-G5L7_ALo_ zjNmQ`QYb-WtV_{GAqN#ChiuAY7$62|&4hNX>Dj}&t&R`vo)2{&q@O{VkY~eh0eB(r zBhW>(elvX%wJI_jVNsRP%4`m&m@8Fa86Hjf!uEBiZFmJSdvtg!F}q~i%Lefu#lZb? z!-pW4TPS_0Xe7|sU_ixlTj@ou&L3Aq#V`1o1H*8O>R%9@mEKQUh%2B%ZBKuv7TEn- zxqQ_1HfAmjPpPRA$ISKdmJeX!gm}DXSlTZPN`T&L0c3G?^|!wukNHSqx;`95Z$Aj%bs-8ZxCH_>f^gUQM!1|GUp5q z*ID1WBp7gCBQFPlNqqNmgHyCZ_sd2oJ4-6J-D6z>x{u%~K?>~v`(==o44rH;6N3sa9Rp(Y8rU-_eBhKTIrgx9DT5T~T-%<(RARNU%2^t{zoZ|4U zEYDLkmBU89$)4quTb(h#I3$#Uy`wihHcuh~0_T4;y0NbU_eJ$uN980K8q2Hd_?zUX z_= zi;(4_wGnSJ;z@=bD<>B1Dj9|xsaTxVci~lzst8|TY*qGevSq--5%L)m9&Zfo#(6_X zDgF5wisA2TcYgM1oLRNr1T393TOZAdIvDLvkLCepF4Ezk)`Xt{F%J2hEJJaGe69EM z!`MwZ+ul7b8San4FbM;XOxygTJ0^S2>#igAMJE{xD8Rsa0Mkhfqfc5$+*n+f-UIqX z90p_V6FCXAV&4K@OAMh7Lk0Vos5-yQ_nS7C7pm~{$KB1XhnY@2d`Ty_#O|NC1JQI| zFgCLiO(;ELZ3q^eD-{*6S*t}EPTG+;x1;oYqFN~;7o0Nj|L-28Z9!-<<*r6 zw4aW4zfl$X0RXpRP;^H$%XXgwPvD7l_;5ELFfI!Yl-?!Cea&Cbnax({_hxD}JsM*5 zoX_feRaLP))}F&-`!v2j^!*^6OUHQma$Qw-*_5^L3qf3E)Fj=9nx)Ai{y$YJ+;G$o zP0k6ZkW2AYMcfNNix@gXQJH8Wb*j7g&ZVGq=9WH)kG80mWDI?9|3(ZUu`PGlO+t6@ z$-TH}jsUh(mS||i1?ozJRtp`b>8V@cxPgXm&qAZ8bXNhk9JWd9lyZ|Kh%PeupdvCa zU+{YXy!NLT@qt9iT4JWn-P+d}C9Po-T%uo@gf$9L(%}L{y36C68VD@H1^9QI&N!}`?(C1=1 z@{=`JysRq*8XJS<@%mUyaoBGIxI23$U)r$jn-LhgNw)E$5P}WK-zzr%2xf?QnEz?I zmk*}q_L>V$b+OITIdSdVZ**+nqRrPTR=ao4biKU_=XyhF0O}IPcT|*p)~JHb?EeRn zMcd}Rq^)e$4QC~Q^yX`ab2T;N`c%=x8HsBbl9)~Uv~C=)&|7Hl@Ck-=e7km^-1g6a z3WiHpY-O-*Z4_!{0pCPiQQ?&BH-|EurSj+xr9rCQ^8W7dQQ^%rY2x^-Dy7v`lT>{$ z!G1aijcr)Iy}u$Mhhqkge7mE+=vCL?tgtF@K6TgMuE5c|ONZszVty*Ed16{JR*RJ+ zfQD8P&+F{(CBuC(diP%WEEpk7t}Uqb@-S-ylfC4a{a#yzZLpO{N}vI-w^TRG6hN$( z6OoR-&n<%(kE_r%(J!KdW78@d#}SPqZ#q^K5#vMMfeM|reJ2dKj`GuA1gGaQt>7I% zwN?|49C#_CI)>ip%y!PurE3eiXoxbJ@|)Ju+b9NAM;H-JLUiwS2{;{ajXBa$h=Y)W zSH!%*@sc)|KGi@~fYWYACXO9`igY=c!yMwes8%0jeY|MzQeo6#fPyUflh4K}@qs5m zf==Kosi~GC|9~sXuTY^^SZ9_cS~6~l>}CL?xCr!C0fsG#i<~m1i$CJnxp)8lwD$gH zw6*E(3*@p6H1%6pDKeU#1*B<~`&y11>8fXYHQ0HG~H8 zu*v@JCGFO3f`jImLoAN2NGUb(j8i_>D5JR29x7BZAT^xywIg{8{T($`i_|^o!(wq~ z;${oMnK(XSuIjxxMZ|IpT7n$-G7ZROzXIWhsyRIClPiYbBr`qF9yJW(6Rqo8CxtckSe9#Ltm19Zi6?Q6 zREpHRdDLqc#jo&E<6O$+?%!`+M=aD@?_AJ0;2ZlQ z936rD9G_i81V<7eB+9C!xiYbe^AA{Dub>WEJ!j7h9m1gYd=m#6MV(}lpr$AvT**=b z!6BiYkssccVNglzH*=rMSGr506ONLB8uM)LtP|;;9m%#uAnzLKY1cr5at{S;kJ7|W zYEVfd;{7-t!jO{zcQQyT;5C5r2z`Gmg+UIt@URb2?_y1V>Z=D+z&TMS<1+NG5F>x- zpwi3j*Am62T8pekIxFW4LyiE7+=rVd2 z5j~X!=a-(7B@CtU6&Z-?wAepIM0+B?9@Nu91mScpXyPnvP#2W6 z`o?xs?17sW)yh5D=Dk4E#SsSfA=OJh4Tg*P3dD4fTv-w@UAq+yocG&-W^=qW+Fs-=cAD+Ng} zP0@fDCC?Hk{hR@_7N4_J zuNj`5;m(dTcp4ukik>|FfhEuPz&5ntV_Xu4lTh4$OtISh=uE?i1Fp+yxZ+D1IRK4t z@zi1U3{1HHxQ*i}qgk+x2gC&)!HGg=NSCoQSGIhzrI@3sNNGiF@w_idDY?KWucXeEFjZ3L+QXpRpC$~t!gh~+@? zg|8uaXVLAHN$XrZz>ni7DD+I?*{k>T1V6Nb>d885YFF=?uczFtUx8S|^qfGAVMS6= zV8mmKdk#qV9%mbfF~G!BzO?;7viFPnKSHZ#q2_;v2B3&!vc?CSgEtXyhDgvQTWs-~ znDO$HwZ}k-qD+z<&5;yCWoZmB18lXLrgIk_11=Se;!^Mu50cP6%o&FR`-12t-#1U0=RIaya)f`<6M|bDu1B&H`v2c0-V+7 zSV+H#gUYqdjyB1>vLtJAlUm=AD(1|*hW5r%nhqv26^fDICv)ig$W>9KA^_5g%-cbs!tAJwNDY*nb zWbrhN!n*EiN-FTnWPe6r~Ema_$nfyt^Af`Nutx zn2@U~bcUYNPSolAjHWRYbC}EPshi(lfDHo@kU%L^^<9-}` zNP#Xk6c`4Dc$fEydxVVvBaG%Chui#*)RqWi0AR8JjKcv|iPrg!ktvtX*M)qbbonpX4<=?E@=wrQ@U3kC=1VhP!LWZqAP0tt!{WjBkJT^P{jU|u{|E@c zCz2hMj)654G}ZdW`48-u)>~>!$ld?CKyv)RY1fOng-5gORg1pnip{#I%?X zurtG??oUbnGfLR(N5KgS{5nws4)rJIeoztqOB{Z#>M~n7eEbC%*^J-+JhDI6dLxWt z0U$4!?41ORVyP~yxPs}P-Ts?mNq~_*K-mcMHl1NlN2}fU9a7;#ydI;XLGM1Oo`nF; zPZRvD5B-T>9v)(#m>ngM!;0!P7DzKqTGiN5lfu0T;QL}4q$>=T(RjShh`ko?h|<@u z(VLNwRzaHRf_E5h#xB3o3J26#J|QlrHq877^RQ?2=-0 zdhS4Qtg4q-_11`%D_>zC5UE8}gAfYFA%MnF4?$782*`;Qnsr!#7Ec5J6VqGIkNarC zAmjdAYSfoc<9vI?I&*G*_`B*D?8?OQBMNZ5VsU}U_ZV);|9}q8fAWp1&+DR~IPZx^ zUIs8rlETS7f=p)Rz7BYVK_ye^nQ=x6f^+-s-=T9$K6Ul_0yNo0)zE=BeSqmt_lfCliLJ#7bY`O(FZAvzlA zM@r-fa>EIkz7%TV4RjbdVTXK9ERvV|nG#?Aom{e?=&L8ofSW-nR6r$ha}WtjVzJ(C zr~YVviL^PG+L73WmZ=@-D3M4(V$LhyjY$%@Ii#Bb{2Q;3?(X6UiOJ-*WV5hzK9>-- z)7dNllO(ZzU;`;=fR3N9Lb-(Cjm-h349P1Dm*b6qDW`+NhW~_1=JEi(`iTBWFvds{ zFJ*gGsKRIvhe|32MR`|AwE3OG_@b3o6)J#m;!PBRKz5|^?W1+hYO`6!fixcEO;7H? zEI82-CpBGG$Guw*5xDhbKXH^^BJYO9q02>MB=;FkD+ApI| z_&LV|w0_pmO%69&5}|*7jR0B-$)}tFf`^78`)%ytl#6L#ADs?W4`zB>($j1J-q=w| zN24=v*C@e%2kcV?$Z{mKZ9jrW@pnPklw1dk&5?(R#V|@~B+Kx`THM*_gwLguN=AoS zeVkg{O$sSNWOX83zCwaJ4udw7wua7dW@=jsFR8`(ld?dx?0xMIe*M9|=?n4&WKx!h ziPr2vG$2Fd6Y`-)#Fj5bXguAGxcz!&xU*GoakPaQV%U)())xU3LMjGA@<)AyxLion zcE%k*A!Bpf=OClWgHNPl(N<5f%3{21G}mm0NkZhLpt%2X`y>Te!RUDfS$j z+H`@=Xd_g!oW6U2Fnj9p4l5zBkz%{Fk(X|>r$dl8jy&UoG7h1nKUUMmi0;JEglrTChO6 zL%Kl`knR$syJN2h{e7{|c)$1jamLyI>@ge+MYx}Jub6AjYtDIHFj7BqrF>58z#EIb zjU8dY$j4<&WVWN0>rb?p)(2~9;1TOBu&I}X&Jqk*A(y(22*OC|l?(iGK+r{-EBhq_ zh^#Mz0?hN3S$N4vGE#uTK|?R#97Tz}n5j40LceyoVh07rvf;d(=$Uf3s+oCRP2d{I zrdr&#YyP;&?ZtQf?JNsxaRMO1K`YT{-DFYE*{`GLylP?~*OA~xh+w6}QvVQ>9s$Z% zP507;+)t0w4-Y5nJgET@&wHL`aNxx%Prp6B&R7Z~V#WtWzd^0+aOF4aIY~WoAnz5o z-w`x!N-p9V=u3S?d8d7mR7GpJgLc|d5t%YYV2u7(a@%|K;N*ogo<3-xC zHd5NP4UG+x&KNh{G@MEtYB2t&(Ia85gnH_PjhG%CXtPF4Kv28CeG~59--?>T?JeYZ%^9wS9?bK^0)+AA&z$=PpZdwOE zi7}r}U{0B@oy+1_1DZSn>Ehh(c&_lsS1(?2umexvBO>or#@=VoZ-(f8M?Nng=hgmd zn~@`F)!uZb-4UM$c93_7YeMVxBO*bFvPVxO@@)O^$R~XWGhcFcAeNLrrXsn2V}uNx zlLqIA0rB;_lSde+En#;S+nsCH(o{w+UE z-urc;_erbb?V)NsUzPdA1IQSv+8v#PRRWzt;qo%>Z9~}@my~ddY}Q^+bR@x0PEm$) zr`OBneL30GB{S#U8QPJSJ?>Abhoa?ZRLmz~+Jo%$$|l#_2U;)|PLq}&XBbzG`g630 zn_mcMFz7!co#{W(WeeT;-c{A{DJ>p&e{Ud}H{Z^T$3Cz{v|Kcs`GR!he6xWVzTe%2 zVC*F^liYWkSQ37+0eqGYklfTE*!M&y;L8xUTqI)&{df)k)t!>Cek8nS0bYMMM2TXP1lE>~y z%XwE%=YFy>FDI)W9;p#N{vZO%MFK>=8%gdsRwviKc~p;uk>t~|BlSh6Tw@-14km}a zj5;8J#gA@!kf~Gtk`|#Rhbj2F-VIp(V+ zzEfjS zcbUCh$lF9Vpjmy8!_h z88(1dfhhnrc1{ghjyE-H#Ae~ul}Djj(1wH#ClP|tYcO ze1i?p*zYGtwuDOVl+M-FoJ8$|st=C3GfYq^tXtnKZ`|sBu<{wc7?PMsf_}O)8@kO` zX>y)~waJ{y(rIqOYKVvZ$z~1Y=L!`f5h8X{rR-))FrSIWn;cxFFFae@DxCCl%|Uj2 zQJRxdYFzOkk%Y?J;TF^YSM5!afo37qn~Kb(R1tO^0dj}=ShHHeDb(I?7y%DA%d*Ai1K_9)Fex>Tm|Y!k^m(e zi+<|(Wo7@iV`IFcaoWAb^8y<_5?^heqJk(@hbSK*s(aJIs%)jxRWkBcWpRr9>1~+d zj#p6~0jIX}<|tA%$ymjFyBSALmzgTh$Z{Tin+hw5U%hg@$2a^s-!_di zeDc*>b04mqYnJMN!Z@}Nn=LF)I#K>L_{Gwolny38xs_Y7Caiy>+#h?353^QO+R|RU z_0(ZyS_w~7^Ow6`Op0&hdfcP(zIcJq28&nAvq;e99`5z(k_qW=J5;a3bB7zOtX!%J zB@$zJUAB|no~gZ83O=ap{+!PbDhb+~v#!R___4`l1oec!Wp(T>09l2#5xg;6tl`zV zXh?z0#Mdm~fKXI8M%PsCRDBJWnbt2+^{Cnem};^M4FL8LX@7xU!u=;OFZ_;#3|d4| z0No)8h_c5$zi?Lw12EH#rL+hrqY=5C_pD)uCcu;+Ufk;xOrNvZy4_mb*vm7_!; zOW|td{LkUhA9kxx<#jej%S#(}M8DThFlb7!HoWHf^0|ZqH&u@h2k`wg`80xtpbGoM z8o7kvJpBdtn@yLse&$F;uN&MUP#=(L><(H~)H5R>xFOygiNSSsxq5+^{W%y}B8VP* zJIVAUaVw#*MI>0KJCl*aa582CZLE`Jp465q+0mf-gi@*y*O0s(+{Rz$DYax_>F9EU z{i%2@sC_P$EoddfWKA+Uk3^dUM|?A-Z>rsSdmYFIl!W5bC3q?w=gJV-@hPWuE5k!0 zrzgQZ-AOoe4B|Cts^go*yzKM482-kzn%Q=z%}Y7%g}&rYDu2mjn&xOkvfo? z*Hnk}a&J35AB`WLISN%n!f$8*(QyQRW0=qC<;xlKy}hjRc;+4F58g9P`ihP6F+-q8 zp-!T8YQfuWGh$nJ1_Qn3Zg=Mg;@!mt*R{QQk<2egBuO^%TMP0R>>vX(N-fuT=!~a| zzAfx;Tij5z1zWi`_(71eli6%^`WxmDii1lvOIYL@V+<6fYfncxfHv?b#?vfEu)JH1 zGzTU2&*w@lhVkQSp)lf1Epu-q+iq)(T(dnZs@|uV(=d{{ufF}M%no8H6s`#X;tgo@ zTL@PZ#p38R^x&fZrsnV6fB7v@C3#8DZ-gsdgBF4Jz5h&2Xs6(+V4m?Q$ek*gB zuHfDM_&cmc)3*|!SzHtYRvKTmiHKP2H1)IE$IH}Ddl5G*NlmTGar9mp&!$AT*Lor z*?dBTWw1IFPr7u_y@O1ZyLxM9yQP`xcxuojzlxF)CSmq4x_V~F&(?V(P+SV7@D^av zxb*1ccd7n|mt$6|${8{qxxF^Hr*$AA#Q4g8-2I*0jp{{q=BJ4!vVQnvc(Yu>vo-Sp z+182smP}L7q6DRgPRFr)F7jYQqU+2G;GS1J<`}ZA1W3Hpxw0>VK}qi7;d^G@io3`C z50ll!CS))O^95I~P=6PTbI87ZYD1>QYEWz{fyb^n=!xg^CO72e$cJ)dZp?-!xD|c^ z9^fw0?_Ch`yM+~M-YGHF1?@X(AGq?cKE$GQfl~FK+3F=Jr4wIDUZ-~Ip^%bCtPf@9 zo1R2~e%oOFk*QCP)KNKHhFw#a4TBR*?oH52J<&{PzoaBlgqX6@mkV1oHJ>EnDJRRG zVv?u71C}G%C24HNHAOwn7qLvcqw)+fiNYi&!P|@Yp?HbvhtfT(GnqA0t{&JxX?lO8KG)k9? zs?Me?;>{R{PC(cvD|*D$lKJdurCfrm?4ufFO}L#9<;ilF(1-y>vHrDB<((%i`pe9q z9c~|%_lL9rJT70mLUTsEbYDdLIF@57pADo^-iidW%CytpB`H+n<5#vl+F2? zRJ5Wf5x>Kyh8yD_lL4<)6$WE(g&qb8w6E-d$;Y`z0a{C$7T1~S?GMl88gG)e1ojsE z{NvSSM4Nw&F=*m|ETkrT_AJ$Kv5~e5_XqbZ#Ye&*bw(93xiVg9Q`UedzT6cy5fR3Q zZ6_V0)Q3V=V^s{-(RHL-Wq9t@+{FTQ1j(RSUKASCBhppel4uoy9)uiMi&ELv4M^t2 zw^X00gAV$~9=9}O#r<8(`Hsb)m9rus0!ORZ+@)G3{wAcezCVdOF#=}ly_EJU^Kozk zw+aNe{;P76AdMVq(qcecT%3ub+ z`Ol=g763&$WSG8x=*+=wd;h0;FY%JXhTXY+Bfu<<=9MYlxxU(hG9_qrp@qlI$M8j% z`3SB#GKMjEE%i6gniRnH|NK)l{D#A15`u@Q75K3K8%oDVREha(j~!$NDwZ&D-pB3~ zBOk618Pz{(4BvfZd;1zx+de)R-$8aXlaSQrLb5;CjA>76e9hL(a9%&>+n<~X=Y`fD zTuHEhK5S;*dmWD4Nq>_mWP#p|PFz%Sl0;4(PE@%w87rSv-LrF^xAO8?BQ0p!_?V<> z!q}5b!)x~bo!I6n+(2y38?zqjDd-9m#Ipo}6`+I?BDAj_D1xi0-_#S-Va3*Fz&B_7 zfe#cG8@V_EvlOU<5A&}CjR-I4dvs~W7z}W-JY6L7OoKI|`jL$^JBJlyDDntb^Y^;P zNU#K4S)eg72vEzB8B7}6HKL>eS)16r#ZVRKCVtJ%(3kZC5tIx{El~RWlyRNWXw)aw zKfi%B?TYw(3rIcg+Zs<`=6S-b$NXP!%znhl7V-$OKuM)`?mK3X;HdgC*S~2(z1B~5 z(i=bx3dx>GpRh@5!$|S7XLua+%VyG)VZa9EOdf)`W|@wDX~5N!4dVAM=cmP&T47& zg<|Z7owp&4Y=$qx5c*RjGB`ke0uZRgQ^!|+7B`C&O)-?hLhcWWJPKK`xIh>dA@&s^ ze}5vlIGeG8J^4oMU1xYZ5(BpQGH7mczut)Fch&eT{T@0QHb2nc!zw)AN=(bC__)F< z*iEumQYGsK)0HfpaX^IRY0jK`jsgd;Xj8dUD6hQc-{h5c4}8u&Nu&@oH|cCQ2dC>~ z{)~)%*@9hM0TdB#d*;fb;?{fTTL(BoYY`h*I>E9n!yK-3+YoY@RUQ8CAqa=h!IRDy zQa$)6JgoNstOGS(;e1n{e9J z_xO+JDkC!xi;lhPP3%!#1_3JQ`4*2{c41D0jj-L&UhELoVUG=bL>#6o)AdCu!1iu) z{HGxBl_#lYSZIo3ws_^7?}KCdqdtBn14=Gqic*6=+_yZ;nt zT4=82`Y+=IFf0Y#@Hu}nKG5DCMp(-XBPdDCLpZK`wx93!B@`gT`u}Xu`4<^Bs@!cs zk`GSIgStNV_@+WGpTM5PSI{DqTmC<0!YKa#<>~=UMNMVBVmklu<2?RL788m9d*4w4 ze0C+kG&F;#+bRIfaM_3bM@9)4+yB!{LxgZ$VrpRMcN6pJ{&O1DcX!_fhmIa`0vZrX zvtUm3(odh}#hZ|b-i3X_#FnFE3evE|V^X*5QGOU5 zYJ0Dt#OFY8Xxgguq<(_Wt8}k<&kI^y?T-iR>os{a<6b-nKYmYehm9EfV2@+zU}Xvf z@cs#psf*h%^5s;&{9!#Z4)hQ@#B5%IqCe zQJm;1;&I_qCdcSX3uuDao&%;uX#2vZi7nf6n;3eX&59Zy)bLf9&sZlo_=}EFoxFb@ z?}@ITo?0bNp)BfgJfHd{v!{W^m?WTd0+j%np=QZk0(=-5_2N^pHoKLRNoR96aLT-u z9{~GP8CoD&awtrXHMe9p-y&b};kz^pZNsEQ12KG3T-gDQ0F)Oik!}X`j{uR579?cw z0u#TeuM|KQ*$&*lndKtzxG(BL9W{{|@>Ey&Y5>EGDYG&UlzoN+))xo&n#V9!NyZhh z6Sb~`Bg4xD1HN!Hoctn$RPL>{ z_z;W*?4~KtHsYy@SCN8RM6|YTm@u?bZ0(#A0hOK1*#6VTNubd{o!G6E z(4!~J2HI@HpZkG=3N&tZz~r|m{&&&{l=+~XOa72RY<^zn`h6U;^m5XUmS--FMc+k%ZD#I72*%02r;Xozh&?!tdAS4 z&fqNsd1sx|s{!QGqLU=T0|p-FdW!BYSy#bQ?lg01bu~PZ^SJoT{4A%@VpiMCVr?Y( z(6+_RV)OcQZIDgPJSdBx-xOwS*)}B5h9~1dD|(txlVuq9(a4>Dxm$SycA~^EQLSEK z;4NvhrdbZuKvNLH&_j~Wt>OrBY}soGm?*M5T@wJI zd0$A*Q`w>(y@Al2*d)ib0eEgQc--$>FEQdeW&2o>~1&tt9zU2h4Bvr zs@gfa6S89Z9N+WLp1CY(m7??BC#>nUp4kPm2c@Y!@e(rTQ}5Q)>bq}7-kB%_#2u(+ zg_JkP#b@T;Exd7z$*cIjSp+8s;GZlv;-Zha^#_16%O zw$zy)t5K5F5rJHJLf7ii6b`LaKkjha8*vy~rZf*MaIR9Z|D3A^=jn1iupTt_>aebe z3}!M>$*&SWZt?%R-8>rzK3ZQ(uNi_EWON$6dE!-24|bQ%1ebK%bBtlQmq(>pesZO$ zMyWe`wF5U}%zcq_hDq1(M<|da?S;*}VFaC(rek@p&!crQJmoe=JRVzPwa{rQ5Kl&a z&aO25I1-K6gx#wZg%y+vun(8 zOc~=gNy^2BDQ6<2Y-e+RIaQub$>!g(HK(1esf$vJQ?T9$$H7~zTZt5w2{oV2r@%_8>A~Lda~a*Q*3nU(d(^u1Ht#d}e5A?P?!u}( ze|va*+Lhq+oFp`$Hbr}A@?nqprod}K8^WhSQ9;oKd+GNgCZWQ5K*fPl?3^h3ve9)n z0!MYcRF~X&=e0%1GtqMKfvJWVB!;~fE^%MOi%QgMw2@)RrWX)&!*!-^PzYp z!`+jLgaCxTkK{f~IYSrvBDm_!SzpMo&-as70NjL;tAmLOA~f8uC+gUO5VA>HY_63II`(1OLs8hw^66t6O6K3< zS-nAKXzM^Ze=#Vd!*ek6{MdO0PIYnymPXKnO{ zzpGS^kAsi!2+x%jY_s+w%wigk7^`AaoABr z3i|#-sR4`bpXn8jeaL(she^$dduO&r0or~u_&erZ<7<}{XXoWBoM!!sCm(vA9~{-^ z3}n4m*m!=hd%b3U`_`jkHT3;k44f@l+{`zrqs)FrloFR;nbRxHf#sjsT<~<=rUjI( zJgm9K-fiP0S|4EG|2eUafPDf_A?3PEQa*)|QKi~beiLky4rkZfa`gn-D>sC!BJEPS z2GrlHjmZm}q02odBJv*wiB9bBBDE4eA2r7f312FXyijD{F?vWGiu6_NC?e2TDrFIzup$j8{0t z-N$$JkM`OZ4dGGJ4-cNw0=~}-Ni+Y?bw?a2iP@l6Psyh+U za*erOtaclNd^i>ev_r#<`qs%+n%H`%sR-ki^OTMVs#-7|RO85Ce&a5CC8*)-x4qc;rTeZ0EM(Ky0;%xGXea zXdhQrQ089m?_gBe43Hn5?$;haT1D~5-y*q$5m%01WmJDD8~N;eHXk!ygho>Rz%NAC zZH|OqKvtz?Xgj8RPW24QsD)LBL~xg@kCAR$ zt_0dURX$=Ex>@&dQEI$uZhXI!XfvjSx+KLwACvO~gFj(@St1hSX@6mkmR#& zo#haW(21SU^jTa~I+t9rXLVqW+d(_6+}kJNH1e6UGoDw~wiuZrKSGOATTqDAq=^d?UPcr`;(1n8o6Z}2P@1~Hjn+? zQ1iahNN$x6&-M?`_S>irk5O3PcT|#ce}h3DcM)N|mJ`xgF2zHGS!>i?=<9aLcw*ke zARFrc6REdFt;kU6)v-JE4Nl1$?{v^keO&wg@{nv8wt7J1%Wvx2%cIgd%xtZ1&k+n7 zpQk^>RsHhAv8Kcf7I`+SUz@FT*8mgYF=c$H*=^v;2x*D0WGaBymaB!RonhxttDA`_ctIR=t{+R(>S#8CJf`VX4E1 z3+BDmjP2Xwjh3D4kRmyYTm#arvwK9fo!LiuQAIx%l>**ij> zvUzUl%^}fzZe194b7%$*%)4LqV&BsU8gJQOddcRZ3}yIATovk!o&=Yu<-L}AHTxw# zsZU)C-%cO* zqt9cDUU-Aj-!~;n73+h(bHoLPYnKgeGCkUNC(WUucSo_c3Dan=2P$oH;t_T|RUbxe zlv>Hr_zRt%QGSaj0=}f{aaxt_xb%^>=}t&B#i8`_n$Wl(O7n%`JgJZ*xUZ>Ho_b10 zq%wVgMIN^_@O0C1cv!DBPkw%j2HW4W=lAG;yNVEvUd=T{<1(Zf!DH8Xv_-;t$QoYS zAzYlK8fmihrqh2!CArEr?@OWmvVe>{Dk7Nn$V%zihdy3BjZ!=)G`SFL+ideK_^$;IMm9_-$P8r?a+m ztU|34ohEU|wQsa%S`FG)C@{2>b%8tWZzQGstZyWaGFlTKX49S(rP!!cqdEPMVhSWB5juq zvc~;tq@W>RrC)<_Etr1#buZ>AchAr;0}#&;OP_#7s?2}mL%ns#mBBbPkDDlTj0F3HxWvG2F~M_R9#?aky^w5OM-yTx1h zl1RtDM`RpVzf(f%BFZ0bwHd=A--bLv=w%GQGZW&qUwvTU_s&KV@@UdcG=WBmYK*Lq z2~g0h!N8NGP$+qqDb=xQXi&?sLGaB>r8(!%X?46_BGj95 zT6}kgV-MpvD9uuB>9`PpPqN=QfHIqu;`|MNeS9nK!0g>pVU{5D?~1rtAqS!oacvMI zwZj`u0U|`5pH|M?RQY@wsePdGoGHC~?Q_oq7+G zMSLs)?dJl&0P72>Ue^h(oY@OMZoO>ZrB@iIx7}>&qTxtfj+o%xDF0gL5MEaIdr*T{ zoI)Q4K1+HU##$quO~UiDK}(8fT-oef01xa9Vj?rlmun85Cc(-NzDXmQM_BQtPVJx7lME)?YN_6%oiWdcK> zStN$y7aZ_^@QX2C7}}&n4|JqSsa)-^&s@2?T%q=-RsFBymHjqqn*}&tdq|?9x)nAx zdwjewxv%3d)rw_>=-IwmJ*UWpjR?MMF78JVUU-`g2UYG02*ci=x#_Z7iQZT>`O8o~ z0WhBs9I6q3g{098{=deDkQwMG@{mQM=)}tJ=PS;i7S^}11$cxhba2d2L=uoqNco?o zmF*vx<3Jw{CAp@d(YnXUvC9-&?J&FmcC&`mh68;se~36;tCwQ$hWk4?Fb#u&4Ldw0 z4TOnhI-tZI`rc3e!cHIPVVR*kGT`w_(8ps{1V8}l$H6CcZt?^vV(ibuSg`)QX!R2! zo%H($P_V!@^#ldS(q}+Bbs#hvOQFLw4F3#AveoX4pe1@@#N7BE<)R2EL)<6B!57Z9 z4g@QX69--(sPi>+nM7ezkj{5ZhU`|V1u5NOp(BYC?S;6KKJ_OATsAsArph<#;TX;YHe|k6Yqf!3}!Ewk%V|f4TJ>}m4 zh3w1#%KhCQW33QZRZ(r_-@N_*9sd89v){y!>;2R!dluYfeMz*5Uv!(LInDbi+;AHu z03g7|j9(hZAHBt^$h|b6E-IH6ulE`p%~weOW)JMY^H&3iA*bagVSVkg&`6=KRsB|W z#Yl;QfNqQH8$sPYxr?3FQgn5t^Fv%!Y|GUYVzBhj1OLrd0fXg@8cYErDTg=HAkOnQ z#pWVER}3`ezCVUA3Xp48zk?ZfR;7np_o@e>PNBM2wv{$ql$ zVOr*l0Di2H7?2EZBPjno)L;Yq@E35V`+N{E^skMIeH!#fQUBt{pxs99wf7>|zqVPA z=%7Ce2gVfQ#i0G(-!y^v-wglHRnYNue|@b8|1wkI`aJNzc z^Q&|2UV#K&N>NlC&zDf?U(XaaVkAf{m@KXI(E;O&1Pe&?$2m>fAw-Xj{hc>{P%4WO z@mQxh@68)LuX;%dWXyO1(lq~=W?2f}zotC%>W@7grUoB8 zn<bqT&$(O_q)3_30S}9ItF(A!QZFw4M5ix zG#|@=>t&<-bSs%5ceed-U%c-vcaeURJ4cBY5MEx}U+Op(KG>oUS-Iaj8UNe~9t~xD zf2_P7Cb(Hgsi8M3jVSLAl;C;#`0N<#9O%QZW&YlOca~3N?ydZHa1VF$o%+^u!Y4sk zNH>DT_i$RgS$LK2{T#BtQ#PC-*Mxn4<$&qWgo}sn5QvF;9x4&1B#@^aoTaC8FTeLc z;`}}SzRk&HehPz1-d;6dYv7_Giced!fzE0sPMzD9cS?RHx(fyKS0( z8cQZbtZw>sp?j-~uTL22afp5aU3J*ZDb9Pm)>x{|7r#iv(iCc}r&h!1v*Um`uxtP< z{P9v;mk(s$zu9VnO}D}MGZ4>~Lnq}g$ddzV(GEs0NH>SQ_P&19(^>zXrMLU_%+O%l zIKC37(@xde<=9%xm7B?gdiD$e55zr7)Hu-5jHNvD(=5nhU0&b2sIsj^V7=YP9Or+# zO~NAMr@ukKNCB!pK$A(SiOccyNlYt{-&yST;I?5WmwFm#_dy#$DtvMCD~jhekVz0da{rN<)* z?0vg)jojAJ!O*q*f{l3GA7-(K^9Fjcy9k$ojzH+-ojD`di_0AOo&eX{i)) zlt$Mb0gnE|0MXJYol|g%-1$cT9y%3&So!1htDOi+6o%fy*lSF%gHA~*)5!Kn?3(iU zgiT)|AXYwnv)%`!vwTL&vW#sgGS`P?$Td}P@u4TBO#Xo((f%r4yFT+jwE#h9%8l7Qds#mB{g@z-K(NrCVvoKZbYyyN#my!Unn>QFvL6iw@~O`p8s z29oRzR+b+W(^o-+82F7chD>NPk^t9)V)ZjVQV>n9@p=~YO!Abnl*>F207M3fJ=y|? z#@eI|<9R^^l-s|-Dh_9P=zxvzU;n(5p?EA;@1Q9{^CFha#E>ygR2FRRD26;1p74bm z$n{Gj&?+%oKl*c5;D8m;t!>7x1!(5&=1gx>{;^hn_D~cSqGG=->NkwODYL<-Y$9}c z+@UlkPtuco|Mo`=!x=`vxarXPBjk64Vr;=^>({Mk^E!xQtXlEAY$lNbIjndf>b(gw zF$Zv$4XD_8z4t{0nf>}#&yzy^Mpq?_d>Z*AS$i7gOr1G@aN-IP$Xe}NSqWt}e||mz zM=-iTAypY;6*3S<`G(A%B!t-koV636dS5)YbXg-Tc!5qPNciH#JQe{qK{l?BM!dCQ#5L{74RuyST(Ah);)9l;E!fDQg8LCDhY@XS!O!t!ldRMeOdw z8oxVVB`x3IBeZ=ftQpAIvij)puqZl@bbg9=KE|&PX*EG4>ux8+Q11>TqC$#kg=fc~ z#MH7UjI`6A&`BaeagoSv1I)O#6wAhWv2e@i2@Vq5e9jwjiw&}aS1@u!@!#e%0>yg?dp#<0hM=V z*@hWla!1$8sF2Q+TSL%DR>8qfX4R|d{q-$l?QFp>)n=yVw<(S|EdE;^d()-x1rti- zrM3aXNA9_AFd^{|OkpJ9!VoaAC>|}jNk!nsgU(R-aBSKF_k(4%3UfI|^$$@mJ|YFh zDP{2V0lD@MHQg`*5@`(jb!8Dq@W{BT0uri$;C;d{3{RH*8Q`!GYeJMLVcW+s;VFr2 z7McyZ$R>$-x*gWD+~h%qM8nMtUKLMmG_xGo2P0HMXVx7?e$ zsx{WSj0O$PnK}=a9%RG%vhKSF#F>Z5lRULeI@y{i((d?rHqVU5q!A~d%p{l0q^ZwL zBZjM2W1Vgb*YbLd2@hkp`O3DMOA;1Q0$4Gep!)NA3c*)E@sNp3TtBdQqFC=El^Re5 z92*CImKMyP>p-@ZA4bqiG$|x*Z!ML5T;@C1I}qc_KY8V~`rH>^b+O@Y(k%8>31qwQ z4ZPxYU@*n)!>0$O&6n~Wuuvl+xzIH9mRq50>;~AQ_n<6Mt@t^&!S%`=VxuvK%!52? zOd1MtR>LNpJEI~k);Yswu{RGltC!3#Zm;&n^!9+$HmaIR%>7z+tWa|ed{4VPs7nwg zj0rP0g+-UDwn8QKR_WI$ZO$qG4Sdr;_(ID3>xrC1~ zzrV3*z!JA#@HzV|BY$b_RB>E(s*WsO~8Mn=}2Jur{7BH<7 z-ncBMn4&Q;Qq!YmGR z0d;!3jrc@(S6kpOdCF8k4TAp+(&Gh9{MwAL8-jd-*r6>g`aWJ4sj*S{A=c zLY_qEYeU$kR~BFsVO|{ckaKqvBB{?*ThRldzC&wt6!s|`yFIW?sHtUQes(s5UHC2r zAW-o@f{4Ce{K`1|F0i*@+6;VgH2@li8N{O9k>6j-(y?U4?=)*|=L&$TWHY+Q$}AKg z#en(MXxhwtHrTy!8U!ov?BTWi8duf!2@W<1@rcuUC~+&bt<^3AWjp6_wY!?NfY#_XgZy?I_$s@ z+4#=K@*TT1Mi@i*Jh$m&TyLbw!%0s7ZYX7QtY`qJQQKNITC+TX^|?At2BMdr&j?VU zmyG1w2S)}|GHDkj+h)I=6+nPiIwgfd6fu_OS0Re`_DOd*c z@X3^B+?y5fks5hXT|y9wlxmw9EvFx4_lq_M-AcCap4!D;l)UTU!`9C8$3`k7v*Xf& z#|>KI)g-)K!SyhFiVfMJ1c-he39?QM1tSGgD!qFsJ)!^n=o1{SrDCisMKUlcv0z39*Q8lY(cLEf}O<~_FhO~I33VbnxFJ?kW9k4qG5vr? zx6C-87#4XKo>}Wl9FSGC^2OUe?O1*Uv+W){D^dM=44Z}$lxaHEEGElQj3Yi?NN!~5 z_txI6khz`*foiSKqu-?PxX27Kih_nexgcdaE#QW>Esem8#i3X3&5spkLL+|iBbZdd z97*(N8n;!7H@I^WfWkQ1dh2+*Tq0eyfoPlT>xmLpv!0k_aJF8~lemlgEYen~Fz>^j z3b<@K0b9Q}lh5fV3L>8^G`buTI?Ei)c&gF|aMlaR(!N|P9qgc+1&(Y+hQ~%;=xQ*5 zA=rKy_0z+T;Bf^Qx^+mPJFasw>6g7V9p(bH6^KC^m=P1AOY~}$nY2q-fay%Q+HF(; zmY1H-=~q=f!tJYFu>bKprGfIm^AvU?Hj$g-#95!SxrQzDdBSmE$k9XQvK+!WV@E$v9A3=St};u%2q<8;?f9gXoIen=RCc%dDliNRt&;3Uu~5-eHANmtEy*aH2{Jf`Mw0&_aO9hoG@_vypITTXc>I=Qe=oK zCJVAn-|DS>%7s=d^{Wv%)!2L}7F4tWG0RQEM>=2>bg_bmVNrw};-HewOkk`pk$zyQ zBXsS3s$s>~otIBH#zML3$}m8HJMuxvN`SMGfUd?Z4lO|x7fI(vaC@P}h#(RZ!F6}` zbEJ&K*I;!U(m(i>!nHR8|dKR4p?W!%L?S^^cq~9b-ePwmLDi z2hL|T7y2Yo;~AlAh%AH$({C{zA|PQMkOgAf!kUmQG~fx@66jNewT}11lE(r^G3o`( zB;Gs3ZB%wD#K^Q-OjNW3OgjSLVG_vzL;;60{C2L;K#{v&9(YI1v|!G5IfcMM9w2+V zrKE_$5H?eyRs2@J-cbq~+PXyUjJ%ITC4SQDIzXXpv(g=X`D68Dc`URtiBTQ7#&~PC zzOw0Zv#17{(Uo1XNmypm&kv{U=*WNX1tOA;xoKNC+(N!in7Dgu06Y_V7`9H8VV%90 zlbC^nytn@=$b8KX8`{fcZz#9Nlw6B>kw8GcLTiaE@@9N+pGCaGviMvH@Ho z1mIFU#6-6jmGuX?FS!IrsojUbEZ6W(aZ;Y3`-#9LLDiG6=G;a#fz52=`q3TY1Ludp zPB^=@!vN%0wf15&rk?)TO?$I}5=7Q9j_HRgAc4yXJjaz*@3MUN6Ed5g$th^p>d z9C4<`hPfBbKUfHxrTOT)z~#fuL>1v-D`S~AKvcKC%%o$vWGCeJ64;~m+w)%=p+SgS z!N$+ei8kU9b0|1ZC)0f>0#uQBZ#-JX@!J)yrEC{r2?F`VL$H}F;c;iV>_r~q?>^P* zl|XbDv1+$%tXnd)Ki!_vi_j|LYf^@DYNIhf(z_u&+iq~#DkQa|{qbHoi$8N9$fbm2 z8+-hLJGAzu2$*3_=WoDp46l)f7j;$n!?Fhv74J3}Ehq^o5 zz)o2M;aRfN`uA6ZeDj`^z9byI?)mU}aJDs=#&NtDGYHH2b#qiW!U>H+SKF0LKNppr zhoX~eC<6aBg6`@~ZeXoV7*@{$MRODIIYx@K*?pYgW09H;9Munn*hMmzcF5;V--&*y zP>?P6GQkeV;F_Tcc!{;RPWil`*r3O@(fz{EI6AZT^(fnX1%a0WtSx=q4>#l^1Kmmr z@MUGKYnp>KxAQ5YE35xTfDKGJ8nw7bnGmj^be!mK5Tj2h0=NI}>n*8WGlf4z$OUjL z56@R(2Y}2ncL|Byf#>P==er!>$00LHBVyc2QPq7=$|!V0MwTS3f*XFA&4H;}c0vQ% zi5P8}&Z%}rKgMP>;A$C_RnwzSa{V+tYp40?yDx}s4+rB*HftPAa%Eg=fbIFol|FcK z4{rziisu)i9K~D67vGPM7sfcEpBOh6z5K4`X2ARbJ_*?6qOt(hM`4d_PHwj#A!9oav~3$^W?=ySV{pX9QG~aX%QlJ3*?t*I*-lUkZn{_{wKTq z;Jc|HHhFn^1NATcp_ku@hJ#-DrPSt!+fleKrEy~;(`5fSloOpb+#QE08v^zPly~2R z-%jKkypzkApbD6zH$H>TD#wE67r)o0&qa|212}DGc|CUP*H?>c=SNV_S)W(^P$h9s zCWPl0BKO`8INqJpb8`Z|Mf5L4-A319WQKjhfO8l-+@Mf^D@ovxy)(@9uW4U9^C<1~zjyIp61dHTex-&f#j@{!j(CpOYSx&b^xN9YlZmIW z$nB1c(SZi;akF^gX7O7P({3Q){ZqgkoRYW3_~vc}1;kINhVtYaY{=*kIw zIl|+|dOx_M?R?!t*Kj%;!$^o}&i&xbpo0t7h3P`Jr3sLlk7_NZFGmB_WL`ADC3tG{ zdT;(KaW@=0j$j$_8WiO5W+vjgCAkVw#YSXjR4e#VMp=sMYiw7Q1k6_HApB_}#8%or z2f^(-3iw2?0D&FF#=23qZ9F9R6H{xtphmQu6%-H)(Fuot_Mjfjn1$Mx^e|2@JdU4$fgh8BP50^{8W)+`1 zfjgB&*r`F;gWr83xU;O>Sf)+0s0n+m?C^07Y?5iJ&NhZ6ny(KP#2ohlaQM5#YOndKWY;%jbpeM?F>bor zYGBT7!KR1SU?ODBHT>t-MPRo;70Tt-xdi~g3$;tx$bF9E&4;oL0NO!ED&U%w<9jK$ z3*^4zblCN36y3(I7T;j3)~{zbmY@l&4*&=cnv6IEFmCWql_Gknel{(@q6>V8axwn} z#@#Av=u86Nywu>)4v|RN#7d}pU~yrs_0&5mWE7fijBG=i@WXIg(v7KcAS4c#EW>}KJyAcmVXuG5fbcOg6Uxxno zrM(fkwwbOPCf|QRcJ3#G))|=yvcB7fpJq57xicFNe>DZfW~zCCJQw47c74FO7FbP` zi~tW@xB7K*62mj`%hMg^SmDc0-nSR)BQ1V@7a&wz1%YuA2(IM;?qLDK^uu-(_TIbO zGm(qStvVwlEM|9R z9Ka3KqaRgLi}h+>`#p}OXaZ)H zN_%#9x)LkW4{q~fQ~02A5FKsRV^%NP1hp&~RiOxoKakOx@P~oZICkHuo_LZu(C>W# z?te{xCZ%%V`T zpRL9@CK?(Y$B0(7sDlhiKA9Ih3jXn=%D(n`7p-~uckR_p+B|gvNPo5(Fp(GLab^vU!A4cx` ziyROW!7>6tH8PJ?aNFT}?}=v;@;Me%0ZhVmXZn30viOS-4Du2~DHV~Ms%cCzCd4_< z<#2i$tMMyT4H@^9Qi%l~lwT5D%4>6q&g;Q?MYA(L)hdaGZ-k?Rp}3(k?$t z*M{5mg+F|I9HoSHIvqG+tXXE%;DlvUV;1iv0mjAtU=)JmD`K+jx|UmDIyn7qz402} z;LVOY`1wDc16^Hl7viFQ$;_$94DP}p8ObTXkRuDzb*8>3{b2Z#%7xjJt6q8Vr^D9y zBs(@@D%5|ozj||orTpecj9v4EbcdsAR1*W#y#>Yd*ZeT>jg`5=g0iaQfhdP|6h_GH z@)Wto2;?g^+F*6ofaeQ!4CR2?(@zEYalg4C*C^VbJli!21-tN1aa8wJ(NaJL3%AZ~ zWhb{B1!Mad47$h-`S3%oGRP4*Yd^V0pJ&*o3po1$FlvkGs#k9X3G=whsM~7*C&_%1 z2a`X=cK4>Y7>TA7+JDZ zlqIRg6v~WsQp{AAaD;~Zx0F$gl(e!lmq9)?nG2Ortx^HeG0 zVVrCrrQaECHzm5qK|RH%Y^9=I4WlX&cBCbtcnL6j-n}l-wDN8IxV>AhagWJo&w=>G zE}vapbK)t=uY5$~P^9onU{bumcD(-Tca~w`?eH!476nrjhcky#!nJK;DEESi4a5UJ zWDmD+LUZgWj-^|4xd<^ z^L1-E1?3B$cm~L=D;LhvwyirKnnVp*Vcn z9agabT&&L3tL+l>mhrXpy&3$g2O^QJ=ke&GuZ8Rw`m1auRHmBh^rw_C&KCB&Dz;eG zG0de@Axde_)Th_SHoJuHt*GDH#**S|4N#{STBfYg$1sMq(w76iNL?Px`+EBGK(fbe=uvaQexygcDuYk8-_oJqoY_>?jcq2|<)HnYk+)eZ z+3}huxYm{dA7wSIgz5tJqz1)I6`od>Le z+;MbyGre9fLE_Q4=!9OeAj>?Z@4pKzsx?#Ehy1?9uMv9dwdflbTi1UA>@a(Ui-v7S z&lqhtO3ZhT#@7d)kIDq<3QJlaC$Rdgo7S6WufD{!l|CeOfXcT} z_J4jb(e5X`VGLEniS$L9svuCQb1xnZ@mUlqLIz!far$U0*(8m>d&~chcj_UPjFq^2 z-l#o&!>VBuRuY>$h->4e%ZA7?T#{|HjmK328I)r4>dN#|nKQ@zRJ}w^l5b3DI}}Me zr1PNcR0K#|cCJARk|}4lvW&tUtrh+PLw+ z#IuL1UXkg;x(8QMk9l!p2wx}m%4bfb$ac7Du6^SqnKns29B8=^T@&z%VV2}7W5?-# zr?gR1YGAvtNPST54PocXy(4mw*ol$7bsvJvgKDp7ynNWP@qKC60{KN;O^^PSg~@T4 z$vzq2#>Q1r8s|mLSfdSkPRav$qIpd2)5n+rhj4OWtopikTxW8<5pxUv0cAL;P2oA3 zYF&>y{h2LeI9;Bm7LkBe8IC+}9{Nz>B_iLZEx6op>NDjncQvl|5Z5RCvi$e_`DvEI zic&Y5k+gkA7d=r~I3y)#BZi2I7=10-Qz)~4$o00!?Fi9vYp_KZO%$Rg>U^vA{$bKt z6}CB5;%UcdPn8D*YHR1$K>v?+67rp_yeKTSa2Uv#OHQ>WvE{_6$Ac}IT^6b+Sv*%y zY29%bLjxx!8P4fWm;hNm3JGRgE}|r51~#A5u6E4N<;8!4mw)Oo!1KFh@u^Yrc|ew zFxtgTZIO z!Qi$_XJSV&jZ0Ms2Jji(R4q3jo{p?c4bMZKEsH;_XF0NCrfl#)sOEbXTXeziUEvzW zkc1vN-*wP8R3miwdD9|qH{ps6GuM^cI}M%OIygrlxzZPZc023x`36xu@qhCJNyboU zfR}mx>h>I^YGppqnv{WeEo!;Xl@|yDuar6(6A0&cd*3eV7yyq!P2m zMR%)Rc<**loNW7uI||`TfIL(wcPFB~c=4slAp0Z+lDDag!nX=a`AL9O^m zvs3Rt{=0A)yUT!g3aYDT(uPerP6g3HEgQHLA`7J6kBmw>U2pIC7GD%IJVDebeZ+59 zt$LrYHnU&+D}xC}#aLu+R4PrZ{CANo)p)~_-z0QMg7nF-_pQ`e$i}U8JLTOdjV=7b zU7&A&pzhDp_wQ8PPzF|`GIOgSmQ!4lQ5a~#kCe!I(ub}n(XrD;VmZIIN*hNp)3&j` z8lUXT1!GRQ=v$&(+{7QuL)N?7aQDWmz*YOyE<8Bc-3tPX)38v#@h5i1RjOoQrC(0T4=ioME;|MH$BQsHcCRKy7P;@uXwG_08y^KG`u z*vGz>?`Za-fTLUjq|e=W$?Ab6mruM7af1TLV^Qhgy}jEP76Ay)d$e}H$Yno7+t#N; ztE#R>uBt@ieuKQiKCwB~*~>UDoSEU&*{q)%)yzM7wxP_o>M$$a_#4chIS)^3L{O{4 z1(^7=`CkL|JmW#|S(7+>`26}KH=-qF%&&q|+^r!uaq4wl+{YbfE7n$@a8gtxi$Pwt zJHur1c7f6~&(I_QgxL(m}JF}EYF_UT^?rc+W+f$i%Q5@5!fvHmyE@iH@176jd!$c z^xWr3unT`$XzRnRho)7rt@mudxkGhyhuHWw!e?~<*E^9tYoINhPYc}jNA_=e<_`&) zLlb?e1sRrL9-g!eju^9eo3l3XdAU|~5?gU7c$ul7r*hA4CWWGkQ}QhcLivr!v{yzW zdx(U;X<~ne7U6f7rfD268^1f0;zP5n5BNv^Pupq4KhPDV1blTDU^c%TdEPVQ+9>&GB@evMyNr$s8O^e%BfwhNwJ z)Qhq3-`S%4=?|rsUUUe5j)juNu~V<(ZWfN$fA;+6TBn?Xv814%ztgWL&+Fqnf=1pR zpW765FZ%WRM+Mfm&YA5Bj0(6DyXV*!!O6yTig*zYaZ1zqV8m)MXxG%LF@_JFA$%<| zeZ@>_Dxev^@O|Yf)4YxqJzk#go{j3WJ1$d;4yI-)>_rWc4T0ewS; zFyvVA=1WkUz@t=}nu1$MlE~by3A5Zwi5WUkdx(Pf)MRBIF;1EgWk?KeRP>*M74=xq zM7G3$kqz-wzYP#q(R0V=rw`i0YHA=<)aD!+aAtp`swYvz958J06bTFlhbL<4yPs=Q zQx@|N3d?ZC@q!{U6cB1?K+-Dpm*5c?{H;C98CH=Bn>Cxe!9eh~)mxDnJOUD~Y!<~< z1AH=xsKlAcJ&nP_XV2NPUw8G#Bj54d*DrHr3H~h8)bZ;-NQQlS~FbU@^@2A0D9vOv+ z`qKuiAVFIl-?P99)3bYY^Noeo1@0!;9U@SN=Un;pGPu<`9;JS;OX;>GGx0xEACMDE z7gSvNy>3mICVaF$YX2wDr9H<+ZT}CegHZw8!+Gcn(E87n{uE&ffz8^VgIxcg;9FWS z>d^Jv@yD1&%h&?*w>fbm1EsfHl7RXY+ z{_q)Oh7>xP+#T`5fB*A6bD)CV<(KC;B8UC{?W)KF1~dL>W)H9LP6p?e z2lI#j{OfIq;?_`KXVB$l#pW#}F<|$sXxf>g0-({uslMkR+ly%PykNO+a5i>SXs$4_ zulM%#$ev35j=EuBc+QxIy7b&ntdljI)<6@MmbD*R!4i@v;`)rpM?5+?1~zHCD1V=u z@)=i78C93OV&ArJ5@N}Ay==Q8lC7aA1h)R2+x_SaSOm@*73=@{b}&<6u-&4c{C>Nz z0qVv8-{ua)=N^UtvkU|NWcalbp^gj?$`IaFdQ$mA)qe zsXr0r|2^CWRY0O^o8(7RVBfm_<;xq}QrCn(BW^N)P{kJk$zWD@?kKb^s=FM)HGkN^Mx literal 0 HcmV?d00001 From f2ada0a604b4c0763bda2f64fac53d506d3beca7 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 8 Jul 2019 19:54:24 +0200 Subject: [PATCH 093/211] docs: quick link fixes throughout docs and repo (#3776) * Quick link fixes throughout docs and repo - used markdown link tester to find broken links Signed-off-by: Marko Baricevic * minor change remove slash * pr comments * minor fix * remove docker.develop * remove master tag --- DOCKER/Dockerfile.develop | 34 --------------- DOCKER/README.md | 21 ++++----- docs/DOCS_README.md | 10 ++--- docs/app-dev/abci-cli.md | 6 +-- docs/app-dev/app-development.md | 4 +- docs/architecture/adr-018-ABCI-Validators.md | 9 ++-- docs/introduction/install.md | 4 +- docs/introduction/introduction.md | 4 +- docs/introduction/what-is-tendermint.md | 4 +- docs/networks/docker-compose.md | 6 +-- docs/networks/terraform-and-ansible.md | 2 +- docs/spec/abci/README.md | 4 +- docs/spec/abci/abci.md | 45 ++++++++++---------- docs/spec/abci/client-server.md | 11 +++-- docs/spec/blockchain/encoding.md | 26 +++++------ docs/spec/consensus/consensus.md | 2 +- docs/spec/reactors/mempool/reactor.md | 2 +- docs/spec/software/wal.md | 2 +- docs/tendermint-core/configuration.md | 5 +-- docs/tendermint-core/how-to-read-logs.md | 2 +- docs/tendermint-core/rpc.md | 2 +- 21 files changed, 80 insertions(+), 125 deletions(-) delete mode 100644 DOCKER/Dockerfile.develop diff --git a/DOCKER/Dockerfile.develop b/DOCKER/Dockerfile.develop deleted file mode 100644 index 943b21291..000000000 --- a/DOCKER/Dockerfile.develop +++ /dev/null @@ -1,34 +0,0 @@ -FROM alpine:3.7 - -ENV DATA_ROOT /tendermint -ENV TMHOME $DATA_ROOT - -RUN addgroup tmuser && \ - adduser -S -G tmuser tmuser - -RUN mkdir -p $DATA_ROOT && \ - chown -R tmuser:tmuser $DATA_ROOT - -RUN apk add --no-cache bash curl jq - -ENV GOPATH /go -ENV PATH "$PATH:/go/bin" -RUN mkdir -p /go/src/github.com/tendermint/tendermint && \ - apk add --no-cache go build-base git && \ - cd /go/src/github.com/tendermint/tendermint && \ - git clone https://github.com/tendermint/tendermint . && \ - git checkout develop && \ - make get_tools && \ - make install && \ - cd - && \ - rm -rf /go/src/github.com/tendermint/tendermint && \ - apk del go build-base git - -VOLUME $DATA_ROOT - -EXPOSE 26656 -EXPOSE 26657 - -ENTRYPOINT ["tendermint"] - -CMD ["node", "--moniker=`hostname`", "--proxy_app=kvstore"] diff --git a/DOCKER/README.md b/DOCKER/README.md index 43edce0fc..57e631aaa 100644 --- a/DOCKER/README.md +++ b/DOCKER/README.md @@ -12,28 +12,25 @@ - `0.9.1`, `0.9`, [(Dockerfile)](https://github.com/tendermint/tendermint/blob/809e0e8c5933604ba8b2d096803ada7c5ec4dfd3/DOCKER/Dockerfile) - `0.9.0` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/d474baeeea6c22b289e7402449572f7c89ee21da/DOCKER/Dockerfile) - `0.8.0`, `0.8` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/bf64dd21fdb193e54d8addaaaa2ecf7ac371de8c/DOCKER/Dockerfile) -- `develop` [(Dockerfile)](https://github.com/tendermint/tendermint/blob/master/DOCKER/Dockerfile.develop) - -`develop` tag points to the [develop](https://github.com/tendermint/tendermint/tree/develop) branch. ## Quick reference -* **Where to get help:** - https://cosmos.network/community +- **Where to get help:** + [cosmos.network/ecosystem](https://cosmos.network/ecosystem) -* **Where to file issues:** - https://github.com/tendermint/tendermint/issues +- **Where to file issues:** + [Tendermint Issues](https://github.com/tendermint/tendermint/issues) -* **Supported Docker versions:** +- **Supported Docker versions:** [the latest release](https://github.com/moby/moby/releases) (down to 1.6 on a best-effort basis) ## Tendermint Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine, written in any programming language, and securely replicates it on many machines. -For more background, see the [introduction](https://tendermint.readthedocs.io/en/master/introduction.html). +For more background, see the [the docs](https://tendermint.com/docs/introduction/#quick-start). -To get started developing applications, see the [application developers guide](https://tendermint.readthedocs.io/en/master/getting-started.html). +To get started developing applications, see the [application developers guide](https://tendermint.com/docs/introduction/quick-start.html). ## How to use this image @@ -48,7 +45,7 @@ docker run -it --rm -v "/tmp:/tendermint" tendermint/tendermint node --proxy_app ## Local cluster -To run a 4-node network, see the `Makefile` in the root of [the repo](https://github.com/tendermint/tendermint/master/Makefile) and run: +To run a 4-node network, see the `Makefile` in the root of [the repo](https://github.com/tendermint/tendermint/blob/master/Makefile) and run: ``` make build-linux @@ -60,7 +57,7 @@ Note that this will build and use a different image than the ones provided here. ## License -- Tendermint's license is [Apache 2.0](https://github.com/tendermint/tendermint/master/LICENSE). +- Tendermint's license is [Apache 2.0](https://github.com/tendermint/tendermint/blob/master/LICENSE). ## Contributing diff --git a/docs/DOCS_README.md b/docs/DOCS_README.md index 49c2030a2..5b743cfa9 100644 --- a/docs/DOCS_README.md +++ b/docs/DOCS_README.md @@ -6,14 +6,12 @@ The documentation for Tendermint Core is hosted at: - https://tendermint-staging.interblock.io/docs/ built from the files in this (`/docs`) directory for -[master](https://github.com/tendermint/tendermint/tree/master/docs) -and [develop](https://github.com/tendermint/tendermint/tree/develop/docs), -respectively. +[master](https://github.com/tendermint/tendermint/tree/master/docs) respectively. ## How It Works There is a CircleCI job listening for changes in the `/docs` directory, on both -the `master` and `develop` branches. Any updates to files in this directory +the `master` branch. Any updates to files in this directory on those branches will automatically trigger a website deployment. Under the hood, the private website repository has a `make build-docs` target consumed by a CircleCI job in that repo. @@ -35,7 +33,7 @@ of the sidebar. **NOTE:** Strongly consider the existing links - both within this directory and to the website docs - when moving or deleting files. -Links to directories *MUST* end in a `/`. +Links to directories _MUST_ end in a `/`. Relative links should be used nearly everywhere, having discovered and weighed the following: @@ -101,4 +99,4 @@ We are using [Algolia](https://www.algolia.com) to power full-text search. This ## Consistency Because the build processes are identical (as is the information contained herein), this file should be kept in sync as -much as possible with its [counterpart in the Cosmos SDK repo](https://github.com/cosmos/cosmos-sdk/blob/develop/docs/DOCS_README.md). +much as possible with its [counterpart in the Cosmos SDK repo](https://github.com/cosmos/cosmos-sdk/blob/master/docs/DOCS_README.md). diff --git a/docs/app-dev/abci-cli.md b/docs/app-dev/abci-cli.md index 7e9db91b9..4b21a4b2d 100644 --- a/docs/app-dev/abci-cli.md +++ b/docs/app-dev/abci-cli.md @@ -62,7 +62,7 @@ as `abci-cli` above. The kvstore just stores transactions in a merkle tree. Its code can be found -[here](https://github.com/tendermint/tendermint/blob/develop/abci/cmd/abci-cli/abci-cli.go) +[here](https://github.com/tendermint/tendermint/blob/master/abci/cmd/abci-cli/abci-cli.go) and looks like: ``` @@ -137,7 +137,7 @@ response. The server may be generic for a particular language, and we provide a [reference implementation in -Golang](https://github.com/tendermint/tendermint/tree/develop/abci/server). See the +Golang](https://github.com/tendermint/tendermint/tree/master/abci/server). See the [list of other ABCI implementations](./ecosystem.md) for servers in other languages. @@ -324,7 +324,7 @@ But the ultimate flexibility comes from being able to write the application easily in any language. We have implemented the counter in a number of languages [see the -example directory](https://github.com/tendermint/tendermint/tree/develop/abci/example). +example directory](https://github.com/tendermint/tendermint/tree/master/abci/example). To run the Node.js version, fist download & install [the Javascript ABCI server](https://github.com/tendermint/js-abci): diff --git a/docs/app-dev/app-development.md b/docs/app-dev/app-development.md index c9983beaa..ba21d3a37 100644 --- a/docs/app-dev/app-development.md +++ b/docs/app-dev/app-development.md @@ -48,9 +48,9 @@ open ABCI connection with the application, which hosts an ABCI server. Shown are the request and response types sent on each connection. Most of the examples below are from [kvstore -application](https://github.com/tendermint/tendermint/blob/develop/abci/example/kvstore/kvstore.go), +application](https://github.com/tendermint/tendermint/blob/master/abci/example/kvstore/kvstore.go), which is a part of the abci repo. [persistent_kvstore -application](https://github.com/tendermint/tendermint/blob/develop/abci/example/kvstore/persistent_kvstore.go) +application](https://github.com/tendermint/tendermint/blob/master/abci/example/kvstore/persistent_kvstore.go) is used to show `BeginBlock`, `EndBlock` and `InitChain` example implementations. diff --git a/docs/architecture/adr-018-ABCI-Validators.md b/docs/architecture/adr-018-ABCI-Validators.md index b632da855..f40efca15 100644 --- a/docs/architecture/adr-018-ABCI-Validators.md +++ b/docs/architecture/adr-018-ABCI-Validators.md @@ -2,10 +2,7 @@ ## Changelog -016-08-2018: Follow up from review: - - Revert changes to commit round - - Remind about justification for removing pubkey - - Update pros/cons +016-08-2018: Follow up from review: - Revert changes to commit round - Remind about justification for removing pubkey - Update pros/cons 05-08-2018: Initial draft ## Context @@ -35,11 +32,11 @@ message ValidatorUpdate { } ``` -As noted in ADR-009[https://github.com/tendermint/tendermint/blob/develop/docs/architecture/adr-009-ABCI-design.md], +As noted in ADR-009[https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-009-ABCI-design.md], the `Validator` does not contain a pubkey because quantum public keys are quite large and it would be wasteful to send them all over ABCI with every block. Thus, applications that want to take advantage of the information in BeginBlock -are *required* to store pubkeys in state (or use much less efficient lazy means +are _required_ to store pubkeys in state (or use much less efficient lazy means of verifying BeginBlock data). ### RequestBeginBlock diff --git a/docs/introduction/install.md b/docs/introduction/install.md index 00e04fa0b..0a013bed1 100644 --- a/docs/introduction/install.md +++ b/docs/introduction/install.md @@ -1,9 +1,9 @@ # Install Tendermint The fastest and easiest way to install the `tendermint` binary -is to run [this script](https://github.com/tendermint/tendermint/blob/develop/scripts/install/install_tendermint_ubuntu.sh) on +is to run [this script](https://github.com/tendermint/tendermint/blob/master/scripts/install/install_tendermint_ubuntu.sh) on a fresh Ubuntu instance, -or [this script](https://github.com/tendermint/tendermint/blob/develop/scripts/install/install_tendermint_bsd.sh) +or [this script](https://github.com/tendermint/tendermint/blob/master/scripts/install/install_tendermint_bsd.sh) on a fresh FreeBSD instance. Read the comments / instructions carefully (i.e., reset your terminal after running the script, make sure you are okay with the network connections being made). diff --git a/docs/introduction/introduction.md b/docs/introduction/introduction.md index f80a159ca..4f435bbf5 100644 --- a/docs/introduction/introduction.md +++ b/docs/introduction/introduction.md @@ -122,7 +122,7 @@ consensus engine, and provides a particular application state. ## ABCI Overview The [Application BlockChain Interface -(ABCI)](https://github.com/tendermint/tendermint/tree/develop/abci) +(ABCI)](https://github.com/tendermint/tendermint/tree/master/abci) allows for Byzantine Fault Tolerant replication of applications written in any programming language. @@ -190,7 +190,7 @@ core to the application. The application replies with corresponding response messages. The messages are specified here: [ABCI Message -Types](https://github.com/tendermint/tendermint/blob/develop/abci/README.md#message-types). +Types](https://github.com/tendermint/tendermint/blob/master/abci/README.md#message-types). The **DeliverTx** message is the work horse of the application. Each transaction in the blockchain is delivered with this message. The diff --git a/docs/introduction/what-is-tendermint.md b/docs/introduction/what-is-tendermint.md index a35dd9ec1..0371afc63 100644 --- a/docs/introduction/what-is-tendermint.md +++ b/docs/introduction/what-is-tendermint.md @@ -116,7 +116,7 @@ consensus engine, and provides a particular application state. ## ABCI Overview The [Application BlockChain Interface -(ABCI)](https://github.com/tendermint/tendermint/tree/develop/abci) +(ABCI)](https://github.com/tendermint/tendermint/tree/master/abci) allows for Byzantine Fault Tolerant replication of applications written in any programming language. @@ -184,7 +184,7 @@ core to the application. The application replies with corresponding response messages. The messages are specified here: [ABCI Message -Types](https://github.com/tendermint/tendermint/blob/develop/abci/README.md#message-types). +Types](https://github.com/tendermint/tendermint/blob/master/abci/README.md#message-types). The **DeliverTx** message is the work horse of the application. Each transaction in the blockchain is delivered with this message. The diff --git a/docs/networks/docker-compose.md b/docs/networks/docker-compose.md index 8db49af5e..37b53fafe 100644 --- a/docs/networks/docker-compose.md +++ b/docs/networks/docker-compose.md @@ -78,9 +78,9 @@ cd $GOPATH/src/github.com/tendermint/tendermint rm -rf ./build/node* ``` -## Configuring abci containers +## Configuring abci containers -To use your own abci applications with 4-node setup edit the [docker-compose.yaml](https://github.com/tendermint/tendermint/blob/develop/docker-compose.yml) file and add image to your abci application. +To use your own abci applications with 4-node setup edit the [docker-compose.yaml](https://github.com/tendermint/tendermint/blob/master/docker-compose.yml) file and add image to your abci application. ``` abci0: @@ -129,7 +129,7 @@ To use your own abci applications with 4-node setup edit the [docker-compose.yam ``` -Override the [command](https://github.com/tendermint/tendermint/blob/master/networks/local/localnode/Dockerfile#L12) in each node to connect to it's abci. +Override the [command](https://github.com/tendermint/tendermint/blob/master/networks/local/localnode/Dockerfile#L12) in each node to connect to it's abci. ``` node0: diff --git a/docs/networks/terraform-and-ansible.md b/docs/networks/terraform-and-ansible.md index 122591be0..3ef6056a0 100644 --- a/docs/networks/terraform-and-ansible.md +++ b/docs/networks/terraform-and-ansible.md @@ -8,7 +8,7 @@ testnets on those servers. ## Install NOTE: see the [integration bash -script](https://github.com/tendermint/tendermint/blob/develop/networks/remote/integration.sh) +script](https://github.com/tendermint/tendermint/blob/master/networks/remote/integration.sh) that can be run on a fresh DO droplet and will automatically spin up a 4 node testnet. The script more or less does everything described below. diff --git a/docs/spec/abci/README.md b/docs/spec/abci/README.md index bb1c38b6e..56d5e8aaf 100644 --- a/docs/spec/abci/README.md +++ b/docs/spec/abci/README.md @@ -2,11 +2,11 @@ ABCI is the interface between Tendermint (a state-machine replication engine) and your application (the actual state machine). It consists of a set of -*methods*, where each method has a corresponding `Request` and `Response` +_methods_, where each method has a corresponding `Request` and `Response` message type. Tendermint calls the ABCI methods on the ABCI application by sending the `Request*` messages and receiving the `Response*` messages in return. -All message types are defined in a [protobuf file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +All message types are defined in a [protobuf file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto). This allows Tendermint to run applications written in any programming language. This specification is split as follows: diff --git a/docs/spec/abci/abci.md b/docs/spec/abci/abci.md index abab7f548..31ac85abd 100644 --- a/docs/spec/abci/abci.md +++ b/docs/spec/abci/abci.md @@ -3,9 +3,9 @@ ## Overview The ABCI message types are defined in a [protobuf -file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto). -ABCI methods are split across 3 separate ABCI *connections*: +ABCI methods are split across 3 separate ABCI _connections_: - `Consensus Connection`: `InitChain, BeginBlock, DeliverTx, EndBlock, Commit` - `Mempool Connection`: `CheckTx` @@ -85,7 +85,7 @@ Example: cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, cmn.KVPair{Key: []byte("reason"), Value: []byte("...")}, }, - }, + }, // ... }, } @@ -115,19 +115,19 @@ non-determinism must be fixed and the nodes restarted. Sources of non-determinism in applications may include: - Hardware failures - - Cosmic rays, overheating, etc. + - Cosmic rays, overheating, etc. - Node-dependent state - - Random numbers - - Time + - Random numbers + - Time - Underspecification - - Library version changes - - Race conditions - - Floating point numbers - - JSON serialization - - Iterating through hash-tables/maps/dictionaries + - Library version changes + - Race conditions + - Floating point numbers + - JSON serialization + - Iterating through hash-tables/maps/dictionaries - External Sources - - Filesystem - - Network calls (eg. some external REST API service) + - Filesystem + - Network calls (eg. some external REST API service) See [#56](https://github.com/tendermint/abci/issues/56) for original discussion. @@ -240,9 +240,9 @@ Commit are included in the header of the next block. - `Path (string)`: Path of request, like an HTTP GET path. Can be used with or in liue of Data. - Apps MUST interpret '/store' as a query by key on the - underlying store. The key SHOULD be specified in the Data field. + underlying store. The key SHOULD be specified in the Data field. - Apps SHOULD allow queries over specific types like - '/accounts/...' or '/votes/...' + '/accounts/...' or '/votes/...' - `Height (int64)`: The block height for which you want the query (default=0 returns data for the latest committed block). Note that this is the height of the block containing the @@ -269,7 +269,7 @@ Commit are included in the header of the next block. - Query for data from the application at current or past height. - Optionally return Merkle proof. - Merkle proof includes self-describing `type` field to support many types - of Merkle trees and encoding formats. + of Merkle trees and encoding formats. ### BeginBlock @@ -486,7 +486,7 @@ Commit are included in the header of the next block. - `Votes ([]VoteInfo)`: List of validators addresses in the last validator set with their voting power and whether or not they signed a vote. -### ConsensusParams +### ConsensusParams - **Fields**: - `Block (BlockParams)`: Parameters limiting the size of a block and time between consecutive blocks. @@ -500,17 +500,17 @@ Commit are included in the header of the next block. - `MaxBytes (int64)`: Max size of a block, in bytes. - `MaxGas (int64)`: Max sum of `GasWanted` in a proposed block. - NOTE: blocks that violate this may be committed if there are Byzantine proposers. - It's the application's responsibility to handle this when processing a - block! + It's the application's responsibility to handle this when processing a + block! ### EvidenceParams - **Fields**: - `MaxAge (int64)`: Max age of evidence, in blocks. Evidence older than this is considered stale and ignored. - - This should correspond with an app's "unbonding period" or other - similar mechanism for handling Nothing-At-Stake attacks. - - NOTE: this should change to time (instead of blocks)! + - This should correspond with an app's "unbonding period" or other + similar mechanism for handling Nothing-At-Stake attacks. + - NOTE: this should change to time (instead of blocks)! ### ValidatorParams @@ -532,4 +532,3 @@ Commit are included in the header of the next block. - `Type (string)`: Type of Merkle proof and how it's encoded. - `Key ([]byte)`: Key in the Merkle tree that this proof is for. - `Data ([]byte)`: Encoded Merkle proof for the key. - diff --git a/docs/spec/abci/client-server.md b/docs/spec/abci/client-server.md index 5ac7b3eb4..94485f0d9 100644 --- a/docs/spec/abci/client-server.md +++ b/docs/spec/abci/client-server.md @@ -9,7 +9,7 @@ Applications](./apps.md). ## Message Protocol The message protocol consists of pairs of requests and responses defined in the -[protobuf file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +[protobuf file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto). Some messages have no fields, while others may include byte-arrays, strings, integers, or custom protobuf types. @@ -33,9 +33,9 @@ The latter two can be tested using the `abci-cli` by setting the `--abci` flag appropriately (ie. to `socket` or `grpc`). See examples, in various stages of maintenance, in -[Go](https://github.com/tendermint/tendermint/tree/develop/abci/server), +[Go](https://github.com/tendermint/tendermint/tree/master/abci/server), [JavaScript](https://github.com/tendermint/js-abci), -[Python](https://github.com/tendermint/tendermint/tree/develop/abci/example/python3/abci), +[Python](https://github.com/tendermint/tendermint/tree/master/abci/example/python3/abci), [C++](https://github.com/mdyring/cpp-tmsp), and [Java](https://github.com/jTendermint/jabci). @@ -44,14 +44,13 @@ See examples, in various stages of maintenance, in The simplest implementation uses function calls within Golang. This means ABCI applications written in Golang can be compiled with TendermintCore and run as a single binary. - ### GRPC If GRPC is available in your language, this is the easiest approach, though it will have significant performance overhead. To get started with GRPC, copy in the [protobuf -file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto) +file](https://github.com/tendermint/tendermint/blob/master/abci/types/types.proto) and compile it using the GRPC plugin for your language. For instance, for golang, the command is `protoc --go_out=plugins=grpc:. types.proto`. See the [grpc documentation for more details](http://www.grpc.io/docs/). @@ -107,4 +106,4 @@ received or a block is committed. It is unlikely that you will need to implement a client. For details of our client, see -[here](https://github.com/tendermint/tendermint/tree/develop/abci/client). +[here](https://github.com/tendermint/tendermint/tree/master/abci/client). diff --git a/docs/spec/blockchain/encoding.md b/docs/spec/blockchain/encoding.md index 14d0e786b..170e91605 100644 --- a/docs/spec/blockchain/encoding.md +++ b/docs/spec/blockchain/encoding.md @@ -59,20 +59,20 @@ familiar with amino encoding. You can simply use below table and concatenate Prefix || Length (of raw bytes) || raw bytes ( while || stands for byte concatenation here). -| Type | Name | Prefix | Length | Notes | -| ------------------ | ----------------------------- | ---------- | -------- | ----- | -| PubKeyEd25519 | tendermint/PubKeyEd25519 | 0x1624DE64 | 0x20 | | -| PubKeySecp256k1 | tendermint/PubKeySecp256k1 | 0xEB5AE987 | 0x21 | | -| PrivKeyEd25519 | tendermint/PrivKeyEd25519 | 0xA3288910 | 0x40 | | -| PrivKeySecp256k1 | tendermint/PrivKeySecp256k1 | 0xE1B0F79B | 0x20 | | -| PubKeyMultisigThreshold | tendermint/PubKeyMultisigThreshold | 0x22C1F7E2 | variable | | +| Type | Name | Prefix | Length | Notes | +| ----------------------- | ---------------------------------- | ---------- | -------- | ----- | +| PubKeyEd25519 | tendermint/PubKeyEd25519 | 0x1624DE64 | 0x20 | | +| PubKeySecp256k1 | tendermint/PubKeySecp256k1 | 0xEB5AE987 | 0x21 | | +| PrivKeyEd25519 | tendermint/PrivKeyEd25519 | 0xA3288910 | 0x40 | | +| PrivKeySecp256k1 | tendermint/PrivKeySecp256k1 | 0xE1B0F79B | 0x20 | | +| PubKeyMultisigThreshold | tendermint/PubKeyMultisigThreshold | 0x22C1F7E2 | variable | | ### Example For example, the 33-byte (or 0x21-byte in hex) Secp256k1 pubkey - `020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` - would be encoded as - `EB5AE98721020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` +`020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` +would be encoded as +`EB5AE98721020BD40F225A57ED383B440CF073BC5539D0341F5767D2BF2D78406D00475A2EE9` ### Key Types @@ -170,11 +170,11 @@ We use the RFC 6962 specification of a merkle tree, with sha256 as the hash func Merkle trees are used throughout Tendermint to compute a cryptographic digest of a data structure. The differences between RFC 6962 and the simplest form a merkle tree are that: -1) leaf nodes and inner nodes have different hashes. +1. leaf nodes and inner nodes have different hashes. This is for "second pre-image resistance", to prevent the proof to an inner node being valid as the proof of a leaf. The leaf nodes are `SHA256(0x00 || leaf_data)`, and inner nodes are `SHA256(0x01 || left_hash || right_hash)`. -2) When the number of items isn't a power of two, the left half of the tree is as big as it could be. +2. When the number of items isn't a power of two, the left half of the tree is as big as it could be. (The largest power of two less than the number of items) This allows new leaves to be added with less recomputation. For example: @@ -290,7 +290,7 @@ func computeHashFromAunts(index, total int, leafHash []byte, innerHashes [][]byt ### IAVL+ Tree -Because Tendermint only uses a Simple Merkle Tree, application developers are expect to use their own Merkle tree in their applications. For example, the IAVL+ Tree - an immutable self-balancing binary tree for persisting application state is used by the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/blob/develop/docs/sdk/core/multistore.md) +Because Tendermint only uses a Simple Merkle Tree, application developers are expect to use their own Merkle tree in their applications. For example, the IAVL+ Tree - an immutable self-balancing binary tree for persisting application state is used by the [Cosmos SDK](https://github.com/cosmos/cosmos-sdk/blob/master/docs/clients/lite/specification.md) ## JSON diff --git a/docs/spec/consensus/consensus.md b/docs/spec/consensus/consensus.md index acd07397a..ec6659c96 100644 --- a/docs/spec/consensus/consensus.md +++ b/docs/spec/consensus/consensus.md @@ -120,7 +120,7 @@ A proposal is signed and published by the designated proposer at each round. The proposer is chosen by a deterministic and non-choking round robin selection algorithm that selects proposers in proportion to their voting power (see -[implementation](https://github.com/tendermint/tendermint/blob/develop/types/validator_set.go)). +[implementation](https://github.com/tendermint/tendermint/blob/master/types/validator_set.go)). A proposal at `(H,R)` is composed of a block and an optional latest `PoLC-Round < R` which is included iff the proposer knows of one. This diff --git a/docs/spec/reactors/mempool/reactor.md b/docs/spec/reactors/mempool/reactor.md index d349fc7cc..7e9a2d8fe 100644 --- a/docs/spec/reactors/mempool/reactor.md +++ b/docs/spec/reactors/mempool/reactor.md @@ -7,7 +7,7 @@ See [this issue](https://github.com/tendermint/tendermint/issues/1503) Mempool maintains a cache of the last 10000 transactions to prevent replaying old transactions (plus transactions coming from other validators, who are continually exchanging transactions). Read [Replay -Protection](../../../../app-development.md#replay-protection) +Protection](../../../app-dev/app-development.md#replay-protection) for details. Sending incorrectly encoded data or data exceeding `maxMsgSize` will result diff --git a/docs/spec/software/wal.md b/docs/spec/software/wal.md index 1f5d712c5..889ce4868 100644 --- a/docs/spec/software/wal.md +++ b/docs/spec/software/wal.md @@ -28,5 +28,5 @@ WAL. Then it will go to precommit, and that time it will work because the private validator contains the `LastSignBytes` and then we’ll replay the precommit from the WAL. -Make sure to read about [WAL corruption](../../../tendermint-core/running-in-production.md#wal-corruption) +Make sure to read about [WAL corruption](../../tendermint-core/running-in-production.md#wal-corruption) and recovery strategies. diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index df05f7c5d..b9f784596 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -315,8 +315,7 @@ namespace = "tendermint" If `create_empty_blocks` is set to `true` in your config, blocks will be created ~ every second (with default consensus parameters). You can regulate -the delay between blocks by changing the `timeout_commit`. E.g. `timeout_commit -= "10s"` should result in ~ 10 second blocks. +the delay between blocks by changing the `timeout_commit`. E.g. `timeout_commit = "10s"` should result in ~ 10 second blocks. **create_empty_blocks = false** @@ -342,7 +341,7 @@ Tendermint will only create blocks if there are transactions, or after waiting ## Consensus timeouts explained There's a variety of information about timeouts in [Running in -production](./running-in-production.html) +production](./running-in-production.md) You can also find more detailed technical explanation in the spec: [The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938). diff --git a/docs/tendermint-core/how-to-read-logs.md b/docs/tendermint-core/how-to-read-logs.md index 54c2c8a32..e852298b8 100644 --- a/docs/tendermint-core/how-to-read-logs.md +++ b/docs/tendermint-core/how-to-read-logs.md @@ -115,7 +115,7 @@ little overview what they do. - `abci-client` As mentioned in [Application Development Guide](../app-dev/app-development.md), Tendermint acts as an ABCI client with respect to the application and maintains 3 connections: mempool, consensus and query. The code used by Tendermint Core can - be found [here](https://github.com/tendermint/tendermint/tree/develop/abci/client). + be found [here](https://github.com/tendermint/tendermint/tree/master/abci/client). - `blockchain` Provides storage, pool (a group of peers), and reactor for both storing and exchanging blocks between peers. - `consensus` The heart of Tendermint core, which is the diff --git a/docs/tendermint-core/rpc.md b/docs/tendermint-core/rpc.md index 4ea5ab0d9..1b8e24426 100644 --- a/docs/tendermint-core/rpc.md +++ b/docs/tendermint-core/rpc.md @@ -4,4 +4,4 @@ The RPC documentation is hosted here: - [https://tendermint.com/rpc/](https://tendermint.com/rpc/) -To update the documentation, edit the relevant `godoc` comments in the [rpc/core directory](https://github.com/tendermint/tendermint/tree/develop/rpc/core). +To update the documentation, edit the relevant `godoc` comments in the [rpc/core directory](https://github.com/tendermint/tendermint/tree/master/rpc/core). From ddee2d641f864d62212bc3ab318f5ff7b64d9aa3 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Wed, 10 Jul 2019 09:48:31 +0100 Subject: [PATCH 094/211] infrastructure for reproducible builds (#3770) * Add deterministic buildsystem * Update CircleCI config * Enable build on all branches for testing purposes * Revert "Enable build on all branches for testing purposes" This reverts commit bf5cf66da94bf9c23787b42995b0409492805f03. * Remove develop from branch filters * Remove dangling reference to develop * Upload binaries too * Build for stable branches too --- .circleci/config.yml | 35 ++- scripts/gitian-build.sh | 201 ++++++++++++++++++ scripts/gitian-descriptors/gitian-darwin.yml | 111 ++++++++++ scripts/gitian-descriptors/gitian-linux.yml | 110 ++++++++++ scripts/gitian-descriptors/gitian-windows.yml | 111 ++++++++++ scripts/gitian-keys/README.md | 29 +++ scripts/gitian-keys/keys.txt | 1 + 7 files changed, 597 insertions(+), 1 deletion(-) create mode 100755 scripts/gitian-build.sh create mode 100644 scripts/gitian-descriptors/gitian-darwin.yml create mode 100644 scripts/gitian-descriptors/gitian-linux.yml create mode 100644 scripts/gitian-descriptors/gitian-windows.yml create mode 100644 scripts/gitian-keys/README.md create mode 100644 scripts/gitian-keys/keys.txt diff --git a/.circleci/config.yml b/.circleci/config.yml index 5836b4546..539dd7ee4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -331,6 +331,34 @@ jobs: docker push "tendermint/tendermint" docker logout + reproducible_builds: + <<: *defaults + steps: + - attach_workspace: + at: /tmp/workspace + - checkout + - setup_remote_docker: + docker_layer_caching: true + - run: + name: Build tendermint + no_output_timeout: 20m + command: | + sudo apt-get install -y ruby + bash -x ./scripts/gitian-build.sh all + for os in darwin linux windows; do + cp gitian-build-${os}/result/tendermint-${os}-res.yml . + cp gitian-build-${os}/build/out/tendermint-*.tar.gz . + rm -rf gitian-build-${os}/ + done + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-darwin-res.yml + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-linux-res.yml + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-windows-res.yml + - store_artifacts: + path: /go/src/github.com/tendermint/tendermint/tendermint-*.tar.gz + workflows: version: 2 test-suite: @@ -340,7 +368,6 @@ workflows: branches: only: - master - - develop - setup_dependencies - test_abci_apps: requires: @@ -364,6 +391,12 @@ workflows: - upload_coverage: requires: - test_cover + - reproducible_builds: + filters: + branches: + only: + - master + - /v[0-9]+\.[0-9]+/ release: jobs: - prepare_build diff --git a/scripts/gitian-build.sh b/scripts/gitian-build.sh new file mode 100755 index 000000000..a7a6acec3 --- /dev/null +++ b/scripts/gitian-build.sh @@ -0,0 +1,201 @@ +#!/bin/bash + +# symbol prefixes: +# g_ -> global +# l_ - local variable +# f_ -> function + +set -euo pipefail + +GITIAN_CACHE_DIRNAME='.gitian-builder-cache' +GO_DEBIAN_RELEASE='1.12.5-1' +GO_TARBALL="golang-debian-${GO_DEBIAN_RELEASE}.tar.gz" +GO_TARBALL_URL="https://salsa.debian.org/go-team/compiler/golang/-/archive/debian/${GO_DEBIAN_RELEASE}/${GO_TARBALL}" + +# Defaults + +DEFAULT_SIGN_COMMAND='gpg --detach-sign' +DEFAULT_TENDERMINT_SIGS=${TENDERMINT_SIGS:-'tendermint.sigs'} +DEFAULT_GITIAN_REPO='https://github.com/devrandom/gitian-builder' +DEFAULT_GBUILD_FLAGS='' +DEFAULT_SIGS_REPO='https://github.com/tendermint/tendermint.sigs' + +# Overrides + +SIGN_COMMAND=${SIGN_COMMAND:-${DEFAULT_SIGN_COMMAND}} +GITIAN_REPO=${GITIAN_REPO:-${DEFAULT_GITIAN_REPO}} +GBUILD_FLAGS=${GBUILD_FLAGS:-${DEFAULT_GBUILD_FLAGS}} + +# Globals + +g_workdir='' +g_gitian_cache='' +g_cached_gitian='' +g_cached_go_tarball='' +g_sign_identity='' +g_sigs_dir='' +g_flag_commit='' + + +f_help() { + cat >&2 <&2 + mkdir "${l_builddir}/inputs/" + cp -v "${g_cached_go_tarball}" "${l_builddir}/inputs/" + done +} + +f_build() { + local l_descriptor + + l_descriptor=$1 + + bin/gbuild --commit tendermint="$g_commit" ${GBUILD_FLAGS} "$l_descriptor" + libexec/stop-target || f_echo_stderr "warning: couldn't stop target" +} + +f_sign_verify() { + local l_descriptor + + l_descriptor=$1 + + bin/gsign -p "${SIGN_COMMAND}" -s "${g_sign_identity}" --destination="${g_sigs_dir}" --release=${g_release} ${l_descriptor} + bin/gverify --destination="${g_sigs_dir}" --release="${g_release}" ${l_descriptor} +} + +f_commit_sig() { + local l_release_name + + l_release_name=$1 + + pushd "${g_sigs_dir}" + git add . || echo "git add failed" >&2 + git commit -m "Add ${l_release_name} reproducible build" || echo "git commit failed" >&2 + popd +} + +f_prep_docker_image() { + pushd $1 + bin/make-base-vm --docker --suite bionic --arch amd64 + popd +} + +f_ensure_cache() { + g_gitian_cache="${g_workdir}/${GITIAN_CACHE_DIRNAME}" + [ -d "${g_gitian_cache}" ] || mkdir "${g_gitian_cache}" + + g_cached_go_tarball="${g_gitian_cache}/${GO_TARBALL}" + if [ ! -f "${g_cached_go_tarball}" ]; then + f_echo_stderr "${g_cached_go_tarball}: cache miss, caching..." + curl -L "${GO_TARBALL_URL}" --output "${g_cached_go_tarball}" + fi + + g_cached_gitian="${g_gitian_cache}/gitian-builder" + if [ ! -d "${g_cached_gitian}" ]; then + f_echo_stderr "${g_cached_gitian}: cache miss, caching..." + git clone ${GITIAN_REPO} "${g_cached_gitian}" + fi +} + +f_demangle_platforms() { + case "${1}" in + all) + printf '%s' 'darwin linux windows' ;; + linux|darwin|windows) + printf '%s' "${1}" ;; + *) + echo "invalid platform -- ${1}" + exit 1 + esac +} + +f_echo_stderr() { + echo $@ >&2 +} + + +while getopts ":cs:h" opt; do + case "${opt}" in + h) f_help ; exit 0 ;; + c) g_flag_commit=y ;; + s) g_sign_identity="${OPTARG}" ;; + esac +done + +shift "$((OPTIND-1))" + +g_platforms=$(f_demangle_platforms "${1}") +g_workdir="$(pwd)" +g_commit="$(git rev-parse HEAD)" +g_sigs_dir=${TENDERMINT_SIGS:-"${g_workdir}/${DEFAULT_TENDERMINT_SIGS}"} + +f_ensure_cache + +f_prep_docker_image "${g_cached_gitian}" + +f_prep_build "${g_platforms}" + +export USE_DOCKER=1 +for g_os in ${g_platforms}; do + g_release="$(git describe --tags --abbrev=9 | sed 's/^v//')-${g_os}" + g_descriptor="${g_workdir}/scripts/gitian-descriptors/gitian-${g_os}.yml" + [ -f ${g_descriptor} ] + g_builddir="$(f_builddir ${g_os})" + + pushd "${g_builddir}" + f_build "${g_descriptor}" + if [ -n "${g_sign_identity}" ]; then + f_sign_verify "${g_descriptor}" + fi + popd + + if [ -n "${g_sign_identity}" -a -n "${g_flag_commit}" ]; then + [ -d "${g_sigs_dir}/.git/" ] && f_commit_sig ${g_release} || f_echo_stderr "couldn't commit, ${g_sigs_dir} is not a git clone" + fi +done + +exit 0 diff --git a/scripts/gitian-descriptors/gitian-darwin.yml b/scripts/gitian-descriptors/gitian-darwin.yml new file mode 100644 index 000000000..03ba1f1a4 --- /dev/null +++ b/scripts/gitian-descriptors/gitian-darwin.yml @@ -0,0 +1,111 @@ +--- +name: "tendermint-darwin" +enable_cache: true +distro: "ubuntu" +suites: +- "bionic" +architectures: +- "amd64" +packages: +- "bsdmainutils" +- "build-essential" +- "ca-certificates" +- "curl" +- "debhelper" +- "dpkg-dev" +- "devscripts" +- "fakeroot" +- "git" +- "golang-any" +- "xxd" +- "quilt" +remotes: +- "url": "https://github.com/tendermint/tendermint.git" + "dir": "tendermint" +files: +- "golang-debian-1.12.5-1.tar.gz" +script: | + set -e -o pipefail + + GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" + # Compile go and configure the environment + export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" + export BUILD_DIR=`pwd` + tar xf "${GO_SRC_TARBALL}" + rm -f "${GO_SRC_TARBALL}" + [ -d "${GO_SRC_RELEASE}/" ] + mv "${GO_SRC_RELEASE}/" go/ + pushd go/ + QUILT_PATCHES=debian/patches quilt push -a + fakeroot debian/rules build RUN_TESTS=false GOCACHE=/tmp/go-cache + popd + + export GOOS=darwin + export GOROOT=${BUILD_DIR}/go + export GOPATH=${BUILD_DIR}/gopath + mkdir -p ${GOPATH}/bin + + export PATH_orig=${PATH} + export PATH=$GOPATH/bin:$GOROOT/bin:$PATH + + export ARCHS='386 amd64' + export GO111MODULE=on + + # Make release tarball + pushd tendermint + VERSION=$(git describe --tags | sed 's/^v//') + COMMIT=$(git rev-parse --short=8 HEAD) + DISTNAME=tendermint-${VERSION} + git archive --format tar.gz --prefix ${DISTNAME}/ -o ${DISTNAME}.tar.gz HEAD + SOURCEDIST=`pwd`/`echo tendermint-*.tar.gz` + popd + + # Correct tar file order + mkdir -p temp + pushd temp + tar xf $SOURCEDIST + rm $SOURCEDIST + find tendermint-* | sort | tar --no-recursion --mode='u+rw,go+r-w,a+X' --owner=0 --group=0 -c -T - | gzip -9n > $SOURCEDIST + popd + + # Prepare GOPATH and install deps + distsrc=${GOPATH}/src/github.com/tendermint/tendermint + mkdir -p ${distsrc} + pushd ${distsrc} + tar --strip-components=1 -xf $SOURCEDIST + go mod download + popd + + # Configure LDFLAGS for reproducible builds + LDFLAGS="-extldflags=-static -buildid=${VERSION} -s -w \ + -X github.com/tendermint/tendermint/version.GitCommit=${COMMIT}" + + # Extract release tarball and build + for arch in ${ARCHS}; do + INSTALLPATH=`pwd`/installed/${DISTNAME}-${arch} + mkdir -p ${INSTALLPATH} + + # Build tendermint binary + pushd ${distsrc} + GOARCH=${arch} GOROOT_FINAL=${GOROOT} go build -a \ + -gcflags=all=-trimpath=${GOPATH} \ + -asmflags=all=-trimpath=${GOPATH} \ + -mod=readonly -tags "tendermint" \ + -ldflags="${LDFLAGS}" \ + -o ${INSTALLPATH}/tendermint ./cmd/tendermint/ + + popd # ${distsrc} + + pushd ${INSTALLPATH} + find -type f | sort | tar \ + --no-recursion --mode='u+rw,go+r-w,a+X' \ + --numeric-owner --sort=name \ + --owner=0 --group=0 -c -T - | gzip -9n > ${OUTDIR}/${DISTNAME}-darwin-${arch}.tar.gz + popd # installed + done + + rm -rf ${distsrc} + + mkdir -p $OUTDIR/src + mv $SOURCEDIST $OUTDIR/src diff --git a/scripts/gitian-descriptors/gitian-linux.yml b/scripts/gitian-descriptors/gitian-linux.yml new file mode 100644 index 000000000..f1c31c40e --- /dev/null +++ b/scripts/gitian-descriptors/gitian-linux.yml @@ -0,0 +1,110 @@ +--- +name: "tendermint-linux" +enable_cache: true +distro: "ubuntu" +suites: +- "bionic" +architectures: +- "amd64" +packages: +- "bsdmainutils" +- "build-essential" +- "ca-certificates" +- "curl" +- "debhelper" +- "dpkg-dev" +- "devscripts" +- "fakeroot" +- "git" +- "golang-any" +- "xxd" +- "quilt" +remotes: +- "url": "https://github.com/tendermint/tendermint.git" + "dir": "tendermint" +files: +- "golang-debian-1.12.5-1.tar.gz" +script: | + set -e -o pipefail + + GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" + # Compile go and configure the environment + export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" + export BUILD_DIR=`pwd` + tar xf "${GO_SRC_TARBALL}" + rm -f "${GO_SRC_TARBALL}" + [ -d "${GO_SRC_RELEASE}/" ] + mv "${GO_SRC_RELEASE}/" go/ + pushd go/ + QUILT_PATCHES=debian/patches quilt push -a + fakeroot debian/rules build RUN_TESTS=false GOCACHE=/tmp/go-cache + popd + + export GOROOT=${BUILD_DIR}/go + export GOPATH=${BUILD_DIR}/gopath + mkdir -p ${GOPATH}/bin + + export PATH_orig=${PATH} + export PATH=$GOPATH/bin:$GOROOT/bin:$PATH + + export ARCHS='386 amd64 arm arm64' + export GO111MODULE=on + + # Make release tarball + pushd tendermint + VERSION=$(git describe --tags | sed 's/^v//') + COMMIT=$(git rev-parse --short=8 HEAD) + DISTNAME=tendermint-${VERSION} + git archive --format tar.gz --prefix ${DISTNAME}/ -o ${DISTNAME}.tar.gz HEAD + SOURCEDIST=`pwd`/`echo tendermint-*.tar.gz` + popd + + # Correct tar file order + mkdir -p temp + pushd temp + tar xf $SOURCEDIST + rm $SOURCEDIST + find tendermint-* | sort | tar --no-recursion --mode='u+rw,go+r-w,a+X' --owner=0 --group=0 -c -T - | gzip -9n > $SOURCEDIST + popd + + # Prepare GOPATH and install deps + distsrc=${GOPATH}/src/github.com/tendermint/tendermint + mkdir -p ${distsrc} + pushd ${distsrc} + tar --strip-components=1 -xf $SOURCEDIST + go mod download + popd + + # Configure LDFLAGS for reproducible builds + LDFLAGS="-extldflags=-static -buildid=${VERSION} -s -w \ + -X github.com/tendermint/tendermint/version.GitCommit=${COMMIT}" + + # Extract release tarball and build + for arch in ${ARCHS}; do + INSTALLPATH=`pwd`/installed/${DISTNAME}-${arch} + mkdir -p ${INSTALLPATH} + + # Build tendermint binary + pushd ${distsrc} + GOARCH=${arch} GOROOT_FINAL=${GOROOT} go build -a \ + -gcflags=all=-trimpath=${GOPATH} \ + -asmflags=all=-trimpath=${GOPATH} \ + -mod=readonly -tags "tendermint" \ + -ldflags="${LDFLAGS}" \ + -o ${INSTALLPATH}/tendermint ./cmd/tendermint/ + + popd # ${distsrc} + + pushd ${INSTALLPATH} + find -type f | sort | tar \ + --no-recursion --mode='u+rw,go+r-w,a+X' \ + --numeric-owner --sort=name \ + --owner=0 --group=0 -c -T - | gzip -9n > ${OUTDIR}/${DISTNAME}-linux-${arch}.tar.gz + popd # installed + done + + rm -rf ${distsrc} + + mkdir -p $OUTDIR/src + mv $SOURCEDIST $OUTDIR/src diff --git a/scripts/gitian-descriptors/gitian-windows.yml b/scripts/gitian-descriptors/gitian-windows.yml new file mode 100644 index 000000000..80b2e60d3 --- /dev/null +++ b/scripts/gitian-descriptors/gitian-windows.yml @@ -0,0 +1,111 @@ +--- +name: "tendermint-windows" +enable_cache: true +distro: "ubuntu" +suites: +- "bionic" +architectures: +- "amd64" +packages: +- "bsdmainutils" +- "build-essential" +- "ca-certificates" +- "curl" +- "debhelper" +- "dpkg-dev" +- "devscripts" +- "fakeroot" +- "git" +- "golang-any" +- "xxd" +- "quilt" +remotes: +- "url": "https://github.com/tendermint/tendermint.git" + "dir": "tendermint" +files: +- "golang-debian-1.12.5-1.tar.gz" +script: | + set -e -o pipefail + + GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" + # Compile go and configure the environment + export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" + export BUILD_DIR=`pwd` + tar xf "${GO_SRC_TARBALL}" + rm -f "${GO_SRC_TARBALL}" + [ -d "${GO_SRC_RELEASE}/" ] + mv "${GO_SRC_RELEASE}/" go/ + pushd go/ + QUILT_PATCHES=debian/patches quilt push -a + fakeroot debian/rules build RUN_TESTS=false GOCACHE=/tmp/go-cache + popd + + export GOOS=windows + export GOROOT=${BUILD_DIR}/go + export GOPATH=${BUILD_DIR}/gopath + mkdir -p ${GOPATH}/bin + + export PATH_orig=${PATH} + export PATH=$GOPATH/bin:$GOROOT/bin:$PATH + + export ARCHS='386 amd64' + export GO111MODULE=on + + # Make release tarball + pushd tendermint + VERSION=$(git describe --tags | sed 's/^v//') + COMMIT=$(git rev-parse --short=8 HEAD) + DISTNAME=tendermint-${VERSION} + git archive --format tar.gz --prefix ${DISTNAME}/ -o ${DISTNAME}.tar.gz HEAD + SOURCEDIST=`pwd`/`echo tendermint-*.tar.gz` + popd + + # Correct tar file order + mkdir -p temp + pushd temp + tar xf $SOURCEDIST + rm $SOURCEDIST + find tendermint-* | sort | tar --no-recursion --mode='u+rw,go+r-w,a+X' --owner=0 --group=0 -c -T - | gzip -9n > $SOURCEDIST + popd + + # Prepare GOPATH and install deps + distsrc=${GOPATH}/src/github.com/tendermint/tendermint + mkdir -p ${distsrc} + pushd ${distsrc} + tar --strip-components=1 -xf $SOURCEDIST + go mod download + popd + + # Configure LDFLAGS for reproducible builds + LDFLAGS="-extldflags=-static -buildid=${VERSION} -s -w \ + -X github.com/tendermint/tendermint/version.GitCommit=${COMMIT}" + + # Extract release tarball and build + for arch in ${ARCHS}; do + INSTALLPATH=`pwd`/installed/${DISTNAME}-${arch} + mkdir -p ${INSTALLPATH} + + # Build tendermint binary + pushd ${distsrc} + GOARCH=${arch} GOROOT_FINAL=${GOROOT} go build -a \ + -gcflags=all=-trimpath=${GOPATH} \ + -asmflags=all=-trimpath=${GOPATH} \ + -mod=readonly -tags "tendermint" \ + -ldflags="${LDFLAGS}" \ + -o ${INSTALLPATH}/tendermint.exe ./cmd/tendermint/ + + popd # ${distsrc} + + pushd ${INSTALLPATH} + find -type f | sort | tar \ + --no-recursion --mode='u+rw,go+r-w,a+X' \ + --numeric-owner --sort=name \ + --owner=0 --group=0 -c -T - | gzip -9n > ${OUTDIR}/${DISTNAME}-windows-${arch}.tar.gz + popd # installed + done + + rm -rf ${distsrc} + + mkdir -p $OUTDIR/src + mv $SOURCEDIST $OUTDIR/src diff --git a/scripts/gitian-keys/README.md b/scripts/gitian-keys/README.md new file mode 100644 index 000000000..f4ad711a9 --- /dev/null +++ b/scripts/gitian-keys/README.md @@ -0,0 +1,29 @@ +## PGP keys of Gitian builders and Tendermint Developers + +The file `keys.txt` contains fingerprints of the public keys of Gitian builders +and active developers. + +The associated keys are mainly used to sign git commits or the build results +of Gitian builds. + +The most recent version of each pgp key can be found on most PGP key servers. + +Fetch the latest version from the key server to see if any key was revoked in +the meantime. +To fetch the latest version of all pgp keys in your gpg homedir, + +```sh +gpg --refresh-keys +``` + +To fetch keys of Gitian builders and active core developers, feed the list of +fingerprints of the primary keys into gpg: + +```sh +while read fingerprint keyholder_name; \ +do gpg --keyserver hkp://subset.pool.sks-keyservers.net \ +--recv-keys ${fingerprint}; done < ./keys.txt +``` + +Add your key to the list if you are a Tendermint core developer or you have +provided Gitian signatures for two major or minor releases of Tendermint. diff --git a/scripts/gitian-keys/keys.txt b/scripts/gitian-keys/keys.txt new file mode 100644 index 000000000..91330ae0b --- /dev/null +++ b/scripts/gitian-keys/keys.txt @@ -0,0 +1 @@ +04160004A8276E40BB9890FBE8A48AE5311D765A Alessio Treglia From fc1eb46587644452e3e0ba495679a7f1d37f85b3 Mon Sep 17 00:00:00 2001 From: Marko Date: Wed, 10 Jul 2019 11:06:03 +0200 Subject: [PATCH 095/211] libs/common: remove heap.go (#3780) * Remove file heap.go - cmn.Heap is not being used in cosmos-sdk, iavl nor tendermint repo. Signed-off-by: Marko Baricevic * changelog entry * Update CHANGELOG_PENDING.md closes #2432 --- CHANGELOG_PENDING.md | 22 ++++---- libs/common/heap.go | 125 ------------------------------------------- 2 files changed, 13 insertions(+), 134 deletions(-) delete mode 100644 libs/common/heap.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 9c4c8b684..cda247cb0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,6 +1,6 @@ ## v0.32.1 -** +\*\* Special thanks to external contributors on this release: @@ -9,33 +9,37 @@ program](https://hackerone.com/tendermint). ### BREAKING CHANGES: -* CLI/RPC/Config +- CLI/RPC/Config -* Apps +- Apps + +- Go API -* Go API - [abci] \#2127 ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI client interface (`abcicli.Client`) to allow for supplying the ABCI `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the mempool indicate to the ABCI app whether a CheckTx request is a recheck or not. - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) + - [libs] \#2432 Remove unused `common/heap.go` file (@marbar3778) -* Blockchain Protocol +- Blockchain Protocol -* P2P Protocol +- P2P Protocol ### FEATURES: + - [node] Refactor `NewNode` to use functional options to make it more flexible and extensible in the future. -- [node] [\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass +- [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass custom reactors to run inside Tendermint node (@ParthDesai) ### IMPROVEMENTS: - - [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) + +- [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) ### BUG FIXES: + - [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling ensurePeers outside of ensurePeersRoutine - [behaviour] Return correct reason in MessageOutOfOrder (@jim380) - diff --git a/libs/common/heap.go b/libs/common/heap.go deleted file mode 100644 index b3bcb9db8..000000000 --- a/libs/common/heap.go +++ /dev/null @@ -1,125 +0,0 @@ -package common - -import ( - "bytes" - "container/heap" -) - -/* - Example usage: - - ``` - h := NewHeap() - - h.Push("msg1", 1) - h.Push("msg3", 3) - h.Push("msg2", 2) - - fmt.Println(h.Pop()) // msg1 - fmt.Println(h.Pop()) // msg2 - fmt.Println(h.Pop()) // msg3 - ``` -*/ -type Heap struct { - pq priorityQueue -} - -func NewHeap() *Heap { - return &Heap{pq: make([]*pqItem, 0)} -} - -func (h *Heap) Len() int64 { - return int64(len(h.pq)) -} - -func (h *Heap) Push(value interface{}, priority int) { - heap.Push(&h.pq, &pqItem{value: value, priority: cmpInt(priority)}) -} - -func (h *Heap) PushBytes(value interface{}, priority []byte) { - heap.Push(&h.pq, &pqItem{value: value, priority: cmpBytes(priority)}) -} - -func (h *Heap) PushComparable(value interface{}, priority Comparable) { - heap.Push(&h.pq, &pqItem{value: value, priority: priority}) -} - -func (h *Heap) Peek() interface{} { - if len(h.pq) == 0 { - return nil - } - return h.pq[0].value -} - -func (h *Heap) Update(value interface{}, priority Comparable) { - h.pq.Update(h.pq[0], value, priority) -} - -func (h *Heap) Pop() interface{} { - item := heap.Pop(&h.pq).(*pqItem) - return item.value -} - -//----------------------------------------------------------------------------- -// From: http://golang.org/pkg/container/heap/#example__priorityQueue - -type pqItem struct { - value interface{} - priority Comparable - index int -} - -type priorityQueue []*pqItem - -func (pq priorityQueue) Len() int { return len(pq) } - -func (pq priorityQueue) Less(i, j int) bool { - return pq[i].priority.Less(pq[j].priority) -} - -func (pq priorityQueue) Swap(i, j int) { - pq[i], pq[j] = pq[j], pq[i] - pq[i].index = i - pq[j].index = j -} - -func (pq *priorityQueue) Push(x interface{}) { - n := len(*pq) - item := x.(*pqItem) - item.index = n - *pq = append(*pq, item) -} - -func (pq *priorityQueue) Pop() interface{} { - old := *pq - n := len(old) - item := old[n-1] - item.index = -1 // for safety - *pq = old[0 : n-1] - return item -} - -func (pq *priorityQueue) Update(item *pqItem, value interface{}, priority Comparable) { - item.value = value - item.priority = priority - heap.Fix(pq, item.index) -} - -//-------------------------------------------------------------------------------- -// Comparable - -type Comparable interface { - Less(o interface{}) bool -} - -type cmpInt int - -func (i cmpInt) Less(o interface{}) bool { - return int(i) < int(o.(cmpInt)) -} - -type cmpBytes []byte - -func (bz cmpBytes) Less(o interface{}) bool { - return bytes.Compare([]byte(bz), []byte(o.(cmpBytes))) < 0 -} From d70871f41b8b4279b4a18505e3b2d49c3962be3e Mon Sep 17 00:00:00 2001 From: Ashley Vega <48948661+ashleyvega@users.noreply.github.com> Date: Wed, 10 Jul 2019 10:27:17 +0100 Subject: [PATCH 096/211] testnet: add consensus_params to testnet config generation (#3781) Also, document time_iota_ms. Closes #3723 Commits: * config: Add ConsensusParams when generating testnet config (#3723) * docs: Add explanation of time_iota_ms (#3723) * Update changelog_pending (#3723) --- CHANGELOG_PENDING.md | 1 + cmd/tendermint/commands/testnet.go | 7 ++++--- docs/tendermint-core/using-tendermint.md | 10 ++++++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index cda247cb0..01b1bfabf 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -43,3 +43,4 @@ program](https://hackerone.com/tendermint). - [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling ensurePeers outside of ensurePeersRoutine - [behaviour] Return correct reason in MessageOutOfOrder (@jim380) +- [config] \#3723 Add consensus_params to testnet config generation; document time_iota_ms (@ashleyvega) diff --git a/cmd/tendermint/commands/testnet.go b/cmd/tendermint/commands/testnet.go index f1dd6f16e..5e2dc1a3a 100644 --- a/cmd/tendermint/commands/testnet.go +++ b/cmd/tendermint/commands/testnet.go @@ -161,9 +161,10 @@ func testnetFiles(cmd *cobra.Command, args []string) error { // Generate genesis doc from generated validators genDoc := &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: "chain-" + cmn.RandStr(6), - Validators: genVals, + ChainID: "chain-" + cmn.RandStr(6), + ConsensusParams: types.DefaultConsensusParams(), + GenesisTime: tmtime.Now(), + Validators: genVals, } // Write genesis file. diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 05d481b2c..8c2fa1e03 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -43,6 +43,11 @@ definition](https://github.com/tendermint/tendermint/blob/master/types/genesis.g - `chain_id`: ID of the blockchain. This must be unique for every blockchain. If your testnet blockchains do not have unique chain IDs, you will have a bad time. The ChainID must be less than 50 symbols. +- `consensus_params` + - `block` + - `time_iota_ms`: Minimum time increment between consecutive blocks (in + milliseconds). If the block header timestamp is ahead of the system clock, + decrease this value. - `validators`: List of initial validators. Note this may be overridden entirely by the application, and may be left empty to make explicit that the application will initialize the validator set with ResponseInitChain. @@ -63,9 +68,10 @@ definition](https://github.com/tendermint/tendermint/blob/master/types/genesis.g "genesis_time": "2018-11-13T18:11:50.277637Z", "chain_id": "test-chain-s4ui7D", "consensus_params": { - "block_size": { + "block": { "max_bytes": "22020096", - "max_gas": "-1" + "max_gas": "-1", + "time_iota_ms": "1000" }, "evidence": { "max_age": "100000" From f05c2a95586cdad1940e3ee2869b213a4bdd3bde Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 10 Jul 2019 13:36:48 +0400 Subject: [PATCH 097/211] p2p: extract ID validation into a separate func (#3754) * p2p: extract ID validation into a separate func - NewNetAddress panics if ID is invalid - NetAddress#Valid returns an error - remove ErrAddrBookInvalidAddrNoID Fixes #2722 * p2p: remove repetitive check in ReceiveAddrs * fix netaddress test --- p2p/netaddress.go | 58 +++++++++++++++++++++++++++--------------- p2p/netaddress_test.go | 15 ++++++++--- p2p/pex/addrbook.go | 8 ++---- p2p/pex/errors.go | 13 +++------- p2p/pex/pex_reactor.go | 16 +----------- 5 files changed, 56 insertions(+), 54 deletions(-) diff --git a/p2p/netaddress.go b/p2p/netaddress.go index d11504525..f39a60543 100644 --- a/p2p/netaddress.go +++ b/p2p/netaddress.go @@ -13,7 +13,7 @@ import ( "strings" "time" - "errors" + "github.com/pkg/errors" ) // NetAddress defines information about a peer on the network @@ -40,7 +40,7 @@ func IDAddressString(id ID, protocolHostPort string) string { // NewNetAddress returns a new NetAddress using the provided TCP // address. When testing, other net.Addr (except TCP) will result in // using 0.0.0.0:0. When normal run, other net.Addr (except TCP) will -// panic. +// panic. Panics if ID is invalid. // TODO: socks proxies? func NewNetAddress(id ID, addr net.Addr) *NetAddress { tcpAddr, ok := addr.(*net.TCPAddr) @@ -53,6 +53,11 @@ func NewNetAddress(id ID, addr net.Addr) *NetAddress { return netAddr } } + + if err := validateID(id); err != nil { + panic(fmt.Sprintf("Invalid ID %v: %v (addr: %v)", id, err, addr)) + } + ip := tcpAddr.IP port := uint16(tcpAddr.Port) na := NewNetAddressIPPort(ip, port) @@ -72,18 +77,11 @@ func NewNetAddressString(addr string) (*NetAddress, error) { } // get ID - idStr := spl[0] - idBytes, err := hex.DecodeString(idStr) - if err != nil { + if err := validateID(ID(spl[0])); err != nil { return nil, ErrNetAddressInvalid{addrWithoutProtocol, err} } - if len(idBytes) != IDByteLength { - return nil, ErrNetAddressInvalid{ - addrWithoutProtocol, - fmt.Errorf("invalid hex length - got %d, expected %d", len(idBytes), IDByteLength)} - } var id ID - id, addrWithoutProtocol = ID(idStr), spl[1] + id, addrWithoutProtocol = ID(spl[0]), spl[1] // get host and port host, portStr, err := net.SplitHostPort(addrWithoutProtocol) @@ -207,22 +205,28 @@ func (na *NetAddress) DialTimeout(timeout time.Duration) (net.Conn, error) { // Routable returns true if the address is routable. func (na *NetAddress) Routable() bool { + if err := na.Valid(); err != nil { + return false + } // TODO(oga) bitcoind doesn't include RFC3849 here, but should we? - return na.Valid() && !(na.RFC1918() || na.RFC3927() || na.RFC4862() || + return !(na.RFC1918() || na.RFC3927() || na.RFC4862() || na.RFC4193() || na.RFC4843() || na.Local()) } // For IPv4 these are either a 0 or all bits set address. For IPv6 a zero // address or one that matches the RFC3849 documentation address format. -func (na *NetAddress) Valid() bool { - if string(na.ID) != "" { - data, err := hex.DecodeString(string(na.ID)) - if err != nil || len(data) != IDByteLength { - return false - } +func (na *NetAddress) Valid() error { + if err := validateID(na.ID); err != nil { + return errors.Wrap(err, "invalid ID") } - return na.IP != nil && !(na.IP.IsUnspecified() || na.RFC3849() || - na.IP.Equal(net.IPv4bcast)) + + if na.IP == nil { + return errors.New("no IP") + } + if na.IP.IsUnspecified() || na.RFC3849() || na.IP.Equal(net.IPv4bcast) { + return errors.New("invalid IP") + } + return nil } // HasID returns true if the address has an ID. @@ -329,3 +333,17 @@ func removeProtocolIfDefined(addr string) string { return addr } + +func validateID(id ID) error { + if len(id) == 0 { + return errors.New("no ID") + } + idBytes, err := hex.DecodeString(string(id)) + if err != nil { + return err + } + if len(idBytes) != IDByteLength { + return fmt.Errorf("invalid hex length - got %d, expected %d", len(idBytes), IDByteLength) + } + return nil +} diff --git a/p2p/netaddress_test.go b/p2p/netaddress_test.go index 7afcab131..e7d82cd77 100644 --- a/p2p/netaddress_test.go +++ b/p2p/netaddress_test.go @@ -11,9 +11,13 @@ import ( func TestNewNetAddress(t *testing.T) { tcpAddr, err := net.ResolveTCPAddr("tcp", "127.0.0.1:8080") require.Nil(t, err) - addr := NewNetAddress("", tcpAddr) - assert.Equal(t, "127.0.0.1:8080", addr.String()) + assert.Panics(t, func() { + NewNetAddress("", tcpAddr) + }) + + addr := NewNetAddress("deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", tcpAddr) + assert.Equal(t, "deadbeefdeadbeefdeadbeefdeadbeefdeadbeef@127.0.0.1:8080", addr.String()) assert.NotPanics(t, func() { NewNetAddress("", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8000}) @@ -106,7 +110,12 @@ func TestNetAddressProperties(t *testing.T) { addr, err := NewNetAddressString(tc.addr) require.Nil(t, err) - assert.Equal(t, tc.valid, addr.Valid()) + err = addr.Valid() + if tc.valid { + assert.NoError(t, err) + } else { + assert.Error(t, err) + } assert.Equal(t, tc.local, addr.Local()) assert.Equal(t, tc.routable, addr.Routable()) } diff --git a/p2p/pex/addrbook.go b/p2p/pex/addrbook.go index 85dd05248..cfe2569ba 100644 --- a/p2p/pex/addrbook.go +++ b/p2p/pex/addrbook.go @@ -586,8 +586,8 @@ func (a *addrBook) addAddress(addr, src *p2p.NetAddress) error { return ErrAddrBookNilAddr{addr, src} } - if !addr.HasID() { - return ErrAddrBookInvalidAddrNoID{addr} + if err := addr.Valid(); err != nil { + return ErrAddrBookInvalidAddr{Addr: addr, AddrErr: err} } if _, ok := a.privateIDs[addr.ID]; ok { @@ -607,10 +607,6 @@ func (a *addrBook) addAddress(addr, src *p2p.NetAddress) error { return ErrAddrBookNonRoutable{addr} } - if !addr.Valid() { - return ErrAddrBookInvalidAddr{addr} - } - ka := a.addrLookup[addr.ID] if ka != nil { // If its already old and the addr is the same, ignore it. diff --git a/p2p/pex/errors.go b/p2p/pex/errors.go index 543056af5..911389a9e 100644 --- a/p2p/pex/errors.go +++ b/p2p/pex/errors.go @@ -56,17 +56,10 @@ func (err ErrAddrBookNilAddr) Error() string { } type ErrAddrBookInvalidAddr struct { - Addr *p2p.NetAddress + Addr *p2p.NetAddress + AddrErr error } func (err ErrAddrBookInvalidAddr) Error() string { - return fmt.Sprintf("Cannot add invalid address %v", err.Addr) -} - -type ErrAddrBookInvalidAddrNoID struct { - Addr *p2p.NetAddress -} - -func (err ErrAddrBookInvalidAddrNoID) Error() string { - return fmt.Sprintf("Cannot add address with no ID %v", err.Addr) + return fmt.Sprintf("Cannot add invalid address %v: %v", err.Addr, err.AddrErr) } diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index 20862d323..557e7ca75 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -350,22 +350,8 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { } for _, netAddr := range addrs { - // Validate netAddr. Disconnect from a peer if it sends us invalid data. - if netAddr == nil { - return errors.New("nil address in pexAddrsMessage") - } - // TODO: extract validating logic from NewNetAddressString - // and put it in netAddr#Valid (#2722) - na, err := p2p.NewNetAddressString(netAddr.String()) - if err != nil { - return fmt.Errorf("%s address in pexAddrsMessage is invalid: %v", - netAddr.String(), - err, - ) - } - // NOTE: we check netAddr validity and routability in book#AddAddress. - err = r.book.AddAddress(na, srcAddr) + err = r.book.AddAddress(netAddr, srcAddr) if err != nil { r.logErrAddrBook(err) // XXX: should we be strict about incoming data and disconnect from a From e9c9c558d7a347b37c9ac6ca770107f84ad24696 Mon Sep 17 00:00:00 2001 From: Marko Date: Wed, 10 Jul 2019 16:40:21 +0200 Subject: [PATCH 098/211] libs/common: remove unused functions (#3784) - The removed functions are not used in Iavl, Cosmos-sdk and tendermint repos - Code-hygenie `whoop whoop` Signed-off-by: Marko Baricevic --- CHANGELOG_PENDING.md | 1 + libs/common/date.go | 43 ----------------------- libs/common/date_test.go | 46 ------------------------- libs/common/io.go | 74 ---------------------------------------- libs/common/os.go | 61 --------------------------------- libs/common/os_test.go | 46 ------------------------- 6 files changed, 1 insertion(+), 270 deletions(-) delete mode 100644 libs/common/date.go delete mode 100644 libs/common/date_test.go delete mode 100644 libs/common/io.go delete mode 100644 libs/common/os_test.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 01b1bfabf..d387f9ecb 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -22,6 +22,7 @@ program](https://hackerone.com/tendermint). not. - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - [libs] \#2432 Remove unused `common/heap.go` file (@marbar3778) + - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) - Blockchain Protocol diff --git a/libs/common/date.go b/libs/common/date.go deleted file mode 100644 index e017a4b41..000000000 --- a/libs/common/date.go +++ /dev/null @@ -1,43 +0,0 @@ -package common - -import ( - "strings" - "time" - - "github.com/pkg/errors" -) - -// TimeLayout helps to parse a date string of the format YYYY-MM-DD -// Intended to be used with the following function: -// time.Parse(TimeLayout, date) -var TimeLayout = "2006-01-02" //this represents YYYY-MM-DD - -// ParseDateRange parses a date range string of the format start:end -// where the start and end date are of the format YYYY-MM-DD. -// The parsed dates are time.Time and will return the zero time for -// unbounded dates, ex: -// unbounded start: :2000-12-31 -// unbounded end: 2000-12-31: -func ParseDateRange(dateRange string) (startDate, endDate time.Time, err error) { - dates := strings.Split(dateRange, ":") - if len(dates) != 2 { - err = errors.New("bad date range, must be in format date:date") - return - } - parseDate := func(date string) (out time.Time, err error) { - if len(date) == 0 { - return - } - out, err = time.Parse(TimeLayout, date) - return - } - startDate, err = parseDate(dates[0]) - if err != nil { - return - } - endDate, err = parseDate(dates[1]) - if err != nil { - return - } - return -} diff --git a/libs/common/date_test.go b/libs/common/date_test.go deleted file mode 100644 index 2c0632477..000000000 --- a/libs/common/date_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package common - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" -) - -var ( - date = time.Date(2015, time.Month(12), 31, 0, 0, 0, 0, time.UTC) - date2 = time.Date(2016, time.Month(12), 31, 0, 0, 0, 0, time.UTC) - zero time.Time -) - -func TestParseDateRange(t *testing.T) { - assert := assert.New(t) - - var testDates = []struct { - dateStr string - start time.Time - end time.Time - errNil bool - }{ - {"2015-12-31:2016-12-31", date, date2, true}, - {"2015-12-31:", date, zero, true}, - {":2016-12-31", zero, date2, true}, - {"2016-12-31", zero, zero, false}, - {"2016-31-12:", zero, zero, false}, - {":2016-31-12", zero, zero, false}, - } - - for _, test := range testDates { - start, end, err := ParseDateRange(test.dateStr) - if test.errNil { - assert.Nil(err) - testPtr := func(want, have time.Time) { - assert.True(have.Equal(want)) - } - testPtr(test.start, start) - testPtr(test.end, end) - } else { - assert.NotNil(err) - } - } -} diff --git a/libs/common/io.go b/libs/common/io.go deleted file mode 100644 index fa0443e09..000000000 --- a/libs/common/io.go +++ /dev/null @@ -1,74 +0,0 @@ -package common - -import ( - "bytes" - "errors" - "io" -) - -type PrefixedReader struct { - Prefix []byte - reader io.Reader -} - -func NewPrefixedReader(prefix []byte, reader io.Reader) *PrefixedReader { - return &PrefixedReader{prefix, reader} -} - -func (pr *PrefixedReader) Read(p []byte) (n int, err error) { - if len(pr.Prefix) > 0 { - read := copy(p, pr.Prefix) - pr.Prefix = pr.Prefix[read:] - return read, nil - } - return pr.reader.Read(p) -} - -// NOTE: Not goroutine safe -type BufferCloser struct { - bytes.Buffer - Closed bool -} - -func NewBufferCloser(buf []byte) *BufferCloser { - return &BufferCloser{ - *bytes.NewBuffer(buf), - false, - } -} - -func (bc *BufferCloser) Close() error { - if bc.Closed { - return errors.New("BufferCloser already closed") - } - bc.Closed = true - return nil -} - -func (bc *BufferCloser) Write(p []byte) (n int, err error) { - if bc.Closed { - return 0, errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.Write(p) -} - -func (bc *BufferCloser) WriteByte(c byte) error { - if bc.Closed { - return errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.WriteByte(c) -} - -func (bc *BufferCloser) WriteRune(r rune) (n int, err error) { - if bc.Closed { - return 0, errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.WriteRune(r) -} - -func (bc *BufferCloser) WriteString(s string) (n int, err error) { - if bc.Closed { - return 0, errors.New("Cannot write to closed BufferCloser") - } - return bc.Buffer.WriteString(s) -} diff --git a/libs/common/os.go b/libs/common/os.go index 7c3fad7ee..0e35524cf 100644 --- a/libs/common/os.go +++ b/libs/common/os.go @@ -1,39 +1,13 @@ package common import ( - "bufio" "fmt" - "io" "io/ioutil" "os" - "os/exec" "os/signal" - "strings" "syscall" ) -var gopath string - -// GoPath returns GOPATH env variable value. If it is not set, this function -// will try to call `go env GOPATH` subcommand. -func GoPath() string { - if gopath != "" { - return gopath - } - - path := os.Getenv("GOPATH") - if len(path) == 0 { - goCmd := exec.Command("go", "env", "GOPATH") - out, err := goCmd.Output() - if err != nil { - panic(fmt.Sprintf("failed to determine gopath: %v", err)) - } - path = string(out) - } - gopath = path - return path -} - type logger interface { Info(msg string, keyvals ...interface{}) } @@ -78,25 +52,6 @@ func EnsureDir(dir string, mode os.FileMode) error { return nil } -func IsDirEmpty(name string) (bool, error) { - f, err := os.Open(name) - if err != nil { - if os.IsNotExist(err) { - return true, err - } - // Otherwise perhaps a permission - // error or some other error. - return false, err - } - defer f.Close() - - _, err = f.Readdirnames(1) // Or f.Readdir(1) - if err == io.EOF { - return true, nil - } - return false, err // Either not empty or error, suits both cases -} - func FileExists(filePath string) bool { _, err := os.Stat(filePath) return !os.IsNotExist(err) @@ -125,19 +80,3 @@ func MustWriteFile(filePath string, contents []byte, mode os.FileMode) { Exit(fmt.Sprintf("MustWriteFile failed: %v", err)) } } - -//-------------------------------------------------------------------------------- - -func Prompt(prompt string, defaultValue string) (string, error) { - fmt.Print(prompt) - reader := bufio.NewReader(os.Stdin) - line, err := reader.ReadString('\n') - if err != nil { - return defaultValue, err - } - line = strings.TrimSpace(line) - if line == "" { - return defaultValue, nil - } - return line, nil -} diff --git a/libs/common/os_test.go b/libs/common/os_test.go deleted file mode 100644 index e8a23ebd6..000000000 --- a/libs/common/os_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package common - -import ( - "os" - "testing" -) - -func TestOSGoPath(t *testing.T) { - // restore original gopath upon exit - path := os.Getenv("GOPATH") - defer func() { - _ = os.Setenv("GOPATH", path) - }() - - err := os.Setenv("GOPATH", "~/testgopath") - if err != nil { - t.Fatal(err) - } - path = GoPath() - if path != "~/testgopath" { - t.Fatalf("should get GOPATH env var value, got %v", path) - } - os.Unsetenv("GOPATH") - - path = GoPath() - if path != "~/testgopath" { - t.Fatalf("subsequent calls should return the same value, got %v", path) - } -} - -func TestOSGoPathWithoutEnvVar(t *testing.T) { - // restore original gopath upon exit - path := os.Getenv("GOPATH") - defer func() { - _ = os.Setenv("GOPATH", path) - }() - - os.Unsetenv("GOPATH") - // reset cache - gopath = "" - - path = GoPath() - if path == "" || path == "~/testgopath" { - t.Fatalf("should get nonempty result of calling go env GOPATH, got %v", path) - } -} From ac232caef364f216504b66050ddbae5a4e79f62e Mon Sep 17 00:00:00 2001 From: Marko Baricevic Date: Thu, 11 Jul 2019 18:34:21 +0200 Subject: [PATCH 099/211] Release branch for v0.32.1 Signed-off-by: Marko Baricevic --- CHANGELOG.md | 79 +++++++++++++++++++++++++++++++++++--------- CHANGELOG_PENDING.md | 27 +-------------- version/version.go | 2 +- 3 files changed, 66 insertions(+), 42 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 76f547a05..c27538ab5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,54 @@ # Changelog +## v0.32.1 + +*July 12, 2019* + +Special thanks to external contributors on this release: + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### BREAKING CHANGES: + +- CLI/RPC/Config + +- Apps + +- Go API + + - [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127) ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI + client interface (`abcicli.Client`) to allow for supplying the ABCI + `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the + mempool indicate to the ABCI app whether a CheckTx request is a recheck or + not. + - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) + - [libs] [\#2432](https://github.com/tendermint/tendermint/issues/2432) Remove unused `common/heap.go` file (@marbar3778) + - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) + +- Blockchain Protocol + +- P2P Protocol + +### FEATURES: + +- [node] Refactor `NewNode` to use functional options to make it more flexible + and extensible in the future. +- [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass + custom reactors to run inside Tendermint node (@ParthDesai) + +### IMPROVEMENTS: + +- [rpc] [\#3700](https://github.com/tendermint/tendermint/issues/3700) Make possible to set absolute paths for TLS cert and key (@climber73) + +### BUG FIXES: + +- [p2p] [\#3338](https://github.com/tendermint/tendermint/issues/3338) Prevent "sent next PEX request too soon" errors by not calling + ensurePeers outside of ensurePeersRoutine +- [behaviour] Return correct reason in MessageOutOfOrder (@jim380) +- [config] [\#3723](https://github.com/tendermint/tendermint/issues/3723) Add consensus_params to testnet config generation; document time_iota_ms (@ashleyvega) + + ## v0.32.0 *June 25, 2019* @@ -21,29 +70,29 @@ program](https://hackerone.com/tendermint). ### BREAKING CHANGES: * CLI/RPC/Config - - [cli] \#3613 Switch from golang/dep to Go Modules to resolve dependencies: + - [cli] [\#3613](https://github.com/tendermint/tendermint/issues/3613) Switch from golang/dep to Go Modules to resolve dependencies: It is recommended to switch to Go Modules if your project has tendermint as a dependency. Read more on Modules here: https://github.com/golang/go/wiki/Modules - [config] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed `leveldb` as generic option for `db_backend`. Must be `goleveldb` or `cleveldb`. - - [rpc] \#3616 Fix field names for `/block_results` response (eg. `results.DeliverTx` + - [rpc] [\#3616](https://github.com/tendermint/tendermint/issues/3616) Fix field names for `/block_results` response (eg. `results.DeliverTx` -> `results.deliver_tx`). See docs for details. - - [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` + - [rpc] [\#3724](https://github.com/tendermint/tendermint/issues/3724) RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` * Apps - - [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, + - [abci] [\#1859](https://github.com/tendermint/tendermint/issues/1859) `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` contains a `type` and a list of `attributes` (list of key-value pairs) allowing for inclusion of multiple distinct events in each response. * Go API - - [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI + - [abci] [\#3193](https://github.com/tendermint/tendermint/issues/3193) Use RequestDeliverTx and RequestCheckTx in the ABCI Application interface - [libs/db] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed deprecated `LevelDBBackend` const If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. - - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID + - [p2p] [\#3521](https://github.com/tendermint/tendermint/issues/3521) Remove NewNetAddressStringWithOptionalID * Blockchain Protocol @@ -52,16 +101,16 @@ program](https://hackerone.com/tendermint). ### FEATURES: ### IMPROVEMENTS: -- [abci/examples] \#3659 Change validator update tx format in the `persistent_kvstore` to use base64 for pubkeys instead of hex (@needkane) -- [consensus] \#3656 Exit if SwitchToConsensus fails -- [p2p] \#3666 Add per channel telemetry to improve reactor observability +- [abci/examples] [\#3659](https://github.com/tendermint/tendermint/issues/3659) Change validator update tx format in the `persistent_kvstore` to use base64 for pubkeys instead of hex (@needkane) +- [consensus] [\#3656](https://github.com/tendermint/tendermint/issues/3656) Exit if SwitchToConsensus fails +- [p2p] [\#3666](https://github.com/tendermint/tendermint/issues/3666) Add per channel telemetry to improve reactor observability - [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) ### BUG FIXES: -- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) -- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) -- [node] \#3716 Fix a bug where `nil` is recorded as node's address -- [node] \#3741 Fix profiler blocking the entire node +- [libs/db] [\#3717](https://github.com/tendermint/tendermint/issues/3717) Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) +- [libs/db] [\#3718](https://github.com/tendermint/tendermint/issues/3718) Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [node] [\#3716](https://github.com/tendermint/tendermint/issues/3716) Fix a bug where `nil` is recorded as node's address +- [node] [\#3741](https://github.com/tendermint/tendermint/issues/3741) Fix profiler blocking the entire node ## v0.31.7 @@ -72,11 +121,11 @@ The regression caused the invalid committed txs to be proposed in blocks over an over again. ### BUG FIXES: -- [mempool] \#3699 Remove all committed txs from the mempool. +- [mempool] [\#3699](https://github.com/tendermint/tendermint/issues/3699) Remove all committed txs from the mempool. This reverts the change from v0.31.6 where we only remove valid txs from the mempool. Note this means malicious proposals can cause txs to be dropped from the mempools of other nodes by including them in blocks before they are valid. - See \#3322. + See [\#3322](https://github.com/tendermint/tendermint/issues/3322). ## v0.31.6 diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d387f9ecb..fd5c4b27e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,4 +1,4 @@ -## v0.32.1 +## v0.32.2 \*\* @@ -15,33 +15,8 @@ program](https://hackerone.com/tendermint). - Go API - - [abci] \#2127 ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI - client interface (`abcicli.Client`) to allow for supplying the ABCI - `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the - mempool indicate to the ABCI app whether a CheckTx request is a recheck or - not. - - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - - [libs] \#2432 Remove unused `common/heap.go` file (@marbar3778) - - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) - -- Blockchain Protocol - -- P2P Protocol - ### FEATURES: -- [node] Refactor `NewNode` to use functional options to make it more flexible - and extensible in the future. -- [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass - custom reactors to run inside Tendermint node (@ParthDesai) - ### IMPROVEMENTS: -- [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) - ### BUG FIXES: - -- [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling - ensurePeers outside of ensurePeersRoutine -- [behaviour] Return correct reason in MessageOutOfOrder (@jim380) -- [config] \#3723 Add consensus_params to testnet config generation; document time_iota_ms (@ashleyvega) diff --git a/version/version.go b/version/version.go index 2d1c41fd5..c5d221551 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.32.0" + TMCoreSemVer = "0.32.1" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.0" From 0f076e5fbef444c18107add3020dda09aea2984f Mon Sep 17 00:00:00 2001 From: Marko Baricevic Date: Thu, 11 Jul 2019 18:37:46 +0200 Subject: [PATCH 100/211] add links to changelog --- CHANGELOG.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c27538ab5..a11b66dea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,13 @@ *July 12, 2019* -Special thanks to external contributors on this release: +Special thanks to external contributors on this release: +@ParthDesai, @climber73, @jim380, @ashleyvega + +This release contains breaking changes to our build and release processes, ABCI namely: +- Add a "Recheck Tx" indicator +- Removal of `db/debugDB`, `common/colors.go`, `errors/errors.go`, `common/heap.go`, `date.go`, `io.go` and + `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermint). From e8926867d8edab5fb208dc6ecb4f25daef0911ab Mon Sep 17 00:00:00 2001 From: Marko Baricevic Date: Thu, 11 Jul 2019 21:22:14 +0200 Subject: [PATCH 101/211] wording change --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a11b66dea..9d7203d7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ Special thanks to external contributors on this release: @ParthDesai, @climber73, @jim380, @ashleyvega -This release contains breaking changes to our build and release processes, ABCI namely: +This release contains breaking changes to our libs folder and ABCI namely: - Add a "Recheck Tx" indicator - Removal of `db/debugDB`, `common/colors.go`, `errors/errors.go`, `common/heap.go`, `date.go`, `io.go` and `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` From 378a0e51bfbc11f8c9891a0fc18a2ff43536f51b Mon Sep 17 00:00:00 2001 From: Andy Nogueira <45477427+andynog@users.noreply.github.com> Date: Thu, 11 Jul 2019 15:31:42 -0400 Subject: [PATCH 102/211] docs: replace priv_validator.json with priv_validator_key.json (#3786) --- docs/tendermint-core/using-tendermint.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 8c2fa1e03..abf382501 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -20,7 +20,7 @@ Initialize the root directory by running: tendermint init ``` -This will create a new private key (`priv_validator.json`), and a +This will create a new private key (`priv_validator_key.json`), and a genesis file (`genesis.json`) containing the associated public key, in `$TMHOME/config`. This is all that's necessary to run a local testnet with one validator. @@ -314,7 +314,7 @@ write-ahead-log](../tendermint-core/running-in-production.md#mempool-wal) ## Tendermint Networks When `tendermint init` is run, both a `genesis.json` and -`priv_validator.json` are created in `~/.tendermint/config`. The +`priv_validator_key.json` are created in `~/.tendermint/config`. The `genesis.json` might look like: ``` @@ -335,7 +335,7 @@ When `tendermint init` is run, both a `genesis.json` and } ``` -And the `priv_validator.json`: +And the `priv_validator_key.json`: ``` { @@ -354,20 +354,20 @@ And the `priv_validator.json`: } ``` -The `priv_validator.json` actually contains a private key, and should +The `priv_validator_key.json` actually contains a private key, and should thus be kept absolutely secret; for now we work with the plain text. Note the `last_` fields, which are used to prevent us from signing conflicting messages. Note also that the `pub_key` (the public key) in the -`priv_validator.json` is also present in the `genesis.json`. +`priv_validator_key.json` is also present in the `genesis.json`. The genesis file contains the list of public keys which may participate in the consensus, and their corresponding voting power. Greater than 2/3 of the voting power must be active (i.e. the corresponding private keys must be producing signatures) for the consensus to make progress. In our case, the genesis file contains the public key of our -`priv_validator.json`, so a Tendermint node started with the default +`priv_validator_key.json`, so a Tendermint node started with the default root directory will be able to make progress. Voting power uses an int64 but must be positive, thus the range is: 0 through 9223372036854775807. Because of how the current proposer selection algorithm works, we do not @@ -453,16 +453,16 @@ not connected to the other peer. The easiest way to add new validators is to do it in the `genesis.json`, before starting the network. For instance, we could make a new -`priv_validator.json`, and copy it's `pub_key` into the above genesis. +`priv_validator_key.json`, and copy it's `pub_key` into the above genesis. -We can generate a new `priv_validator.json` with the command: +We can generate a new `priv_validator_key.json` with the command: ``` tendermint gen_validator ``` Now we can update our genesis file. For instance, if the new -`priv_validator.json` looks like: +`priv_validator_key.json` looks like: ``` { @@ -510,7 +510,7 @@ then the new `genesis.json` will be: ``` Update the `genesis.json` in `~/.tendermint/config`. Copy the genesis -file and the new `priv_validator.json` to the `~/.tendermint/config` on +file and the new `priv_validator_key.json` to the `~/.tendermint/config` on a new machine. Now run `tendermint node` on both machines, and use either From 823d916a11ae413e2b1bca0e838ab5c5ff4beeaa Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 12 Jul 2019 11:06:27 +0200 Subject: [PATCH 103/211] Apply suggestions from code review Comment from PR Co-Authored-By: Ethan Buchman --- CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d7203d7d..788bb8049 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,8 +8,8 @@ Special thanks to external contributors on this release: @ParthDesai, @climber73, @jim380, @ashleyvega This release contains breaking changes to our libs folder and ABCI namely: -- Add a "Recheck Tx" indicator -- Removal of `db/debugDB`, `common/colors.go`, `errors/errors.go`, `common/heap.go`, `date.go`, `io.go` and +- CheckTx requests include a `CheckTxType` enum that can be set to `Recheck` to indicate to the application that this transaction was already checked/validated and certain expensive operations (like checking signatures) can be skipped +- Removed various functions from `libs` pkgs `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` Friendly reminder, we have a [bug bounty @@ -38,8 +38,7 @@ program](https://hackerone.com/tendermint). ### FEATURES: -- [node] Refactor `NewNode` to use functional options to make it more flexible - and extensible in the future. +- [node] Add variadic argument to `NewNode` to support functional options, allowing the Node to be more easily customized. - [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass custom reactors to run inside Tendermint node (@ParthDesai) From 0787b793473102c4d96ec0d16d0f78666ec8a308 Mon Sep 17 00:00:00 2001 From: Marko Baricevic Date: Fri, 12 Jul 2019 11:17:20 +0200 Subject: [PATCH 104/211] add link bump abci Version --- CHANGELOG.md | 10 +--------- version/version.go | 2 +- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 788bb8049..1ea8024f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,10 +17,6 @@ program](https://hackerone.com/tendermint). ### BREAKING CHANGES: -- CLI/RPC/Config - -- Apps - - Go API - [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127) ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI @@ -32,10 +28,6 @@ program](https://hackerone.com/tendermint). - [libs] [\#2432](https://github.com/tendermint/tendermint/issues/2432) Remove unused `common/heap.go` file (@marbar3778) - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) -- Blockchain Protocol - -- P2P Protocol - ### FEATURES: - [node] Add variadic argument to `NewNode` to support functional options, allowing the Node to be more easily customized. @@ -50,7 +42,7 @@ program](https://hackerone.com/tendermint). - [p2p] [\#3338](https://github.com/tendermint/tendermint/issues/3338) Prevent "sent next PEX request too soon" errors by not calling ensurePeers outside of ensurePeersRoutine -- [behaviour] Return correct reason in MessageOutOfOrder (@jim380) +- [behaviour] [\3772](https://github.com/tendermint/tendermint/pull/3772) Return correct reason in MessageOutOfOrder (@jim380) - [config] [\#3723](https://github.com/tendermint/tendermint/issues/3723) Add consensus_params to testnet config generation; document time_iota_ms (@ashleyvega) diff --git a/version/version.go b/version/version.go index c5d221551..91b0ab410 100644 --- a/version/version.go +++ b/version/version.go @@ -23,7 +23,7 @@ const ( TMCoreSemVer = "0.32.1" // ABCISemVer is the semantic version of the ABCI library - ABCISemVer = "0.16.0" + ABCISemVer = "0.16.1" ABCIVersion = ABCISemVer ) From eddb433d7c082efbeaf8974413a36641519ee895 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 12 Jul 2019 09:43:03 -0400 Subject: [PATCH 105/211] abci: Fix documentation regarding CheckTx type update (#3789) * Update ABCI docs to reflect latest changes on PR #3744 * Add note about new Type parameter for CheckTx --- docs/spec/abci/abci.md | 6 ++---- docs/spec/abci/apps.md | 5 ++++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/spec/abci/abci.md b/docs/spec/abci/abci.md index 31ac85abd..275c4dcd8 100644 --- a/docs/spec/abci/abci.md +++ b/docs/spec/abci/abci.md @@ -297,11 +297,9 @@ Commit are included in the header of the next block. - **Request**: - `Tx ([]byte)`: The request transaction bytes - `Type (CheckTxType)`: What type of `CheckTx` request is this? At present, - there are two possible values: `CheckTx_Unchecked` (the default, which says - that a full check is required), and `CheckTx_Checked` (when the mempool is + there are two possible values: `CheckTx_New` (the default, which says + that a full check is required), and `CheckTx_Recheck` (when the mempool is initiating a normal recheck of a transaction). - - `AdditionalData ([]byte)`: Reserved for future use. See - [here](https://github.com/tendermint/tendermint/issues/2127#issuecomment-456661420). - **Response**: - `Code (uint32)`: Response code - `Data ([]byte)`: Result bytes, if any. diff --git a/docs/spec/abci/apps.md b/docs/spec/abci/apps.md index 908ad3eaf..5d4a678d4 100644 --- a/docs/spec/abci/apps.md +++ b/docs/spec/abci/apps.md @@ -65,7 +65,10 @@ begin. After `Commit`, CheckTx is run again on all transactions that remain in the node's local mempool after filtering those included in the block. To prevent the mempool from rechecking all transactions every time a block is committed, set -the configuration option `mempool.recheck=false`. +the configuration option `mempool.recheck=false`. As of Tendermint v0.32.1, +an additional `Type` parameter is made available to the CheckTx function that +indicates whether an incoming transaction is new (`CheckTxType_New`), or a +recheck (`CheckTxType_Recheck`). Finally, the mempool will unlock and new transactions can be processed through CheckTx again. From 1b69c6b56b7dc7941534f2e1f1b12156011f9077 Mon Sep 17 00:00:00 2001 From: Marko Baricevic Date: Fri, 12 Jul 2019 15:59:53 +0200 Subject: [PATCH 106/211] include doc change for abci/app --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ea8024f3..24eec219d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ program](https://hackerone.com/tendermint). client interface (`abcicli.Client`) to allow for supplying the ABCI `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the mempool indicate to the ABCI app whether a CheckTx request is a recheck or - not. + not. see [docs](https://github.com/tendermint/tendermint/blob/eddb433d7c082efbeaf8974413a36641519ee895/docs/spec/abci/apps.md#mempool-connection) - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - [libs] [\#2432](https://github.com/tendermint/tendermint/issues/2432) Remove unused `common/heap.go` file (@marbar3778) - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) From e7ac73177e036a09af2547321d1c9e98130e5349 Mon Sep 17 00:00:00 2001 From: Aditya Date: Fri, 12 Jul 2019 17:29:36 +0000 Subject: [PATCH 107/211] upgrade go.mod (#3793) upgrade levelDB to latest master --- go.mod | 3 +-- go.sum | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index f4a4c6dda..948cf9887 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,6 @@ require ( github.com/gogo/protobuf v1.2.1 github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/protobuf v1.3.0 - github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect github.com/google/gofuzz v1.0.0 // indirect github.com/gorilla/websocket v1.2.0 github.com/hashicorp/hcl v1.0.0 // indirect @@ -41,7 +40,7 @@ require ( github.com/spf13/pflag v1.0.3 // indirect github.com/spf13/viper v1.0.0 github.com/stretchr/testify v1.2.2 - github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e + github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 github.com/tendermint/go-amino v0.14.1 go.etcd.io/bbolt v1.3.3 // indirect golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 diff --git a/go.sum b/go.sum index 2a349d300..14091bbc0 100644 --- a/go.sum +++ b/go.sum @@ -39,6 +39,8 @@ github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= +github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= @@ -103,6 +105,9 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e h1:91EeXI4y4ShkyzkMqZ7QP/ZTIqwXp3RuDu5WFzxcFAs= github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE= +github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 h1:1oFLiOyVl+W7bnBzGhf7BbIv9loSFQcieWWYIjLqcAw= +github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= From 6d96cf4f055492e34e803c30d834bdfc714ce17d Mon Sep 17 00:00:00 2001 From: Karoly Albert Szabo Date: Sat, 13 Jul 2019 07:57:43 +0200 Subject: [PATCH 108/211] tm-monitor: update build-docker Makefile target (#3790) - build docker needs go111module on - switch to golang:1.12 to have git available Signed-off-by: Karoly Albert Szabo --- tools/tm-monitor/Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/tm-monitor/Makefile b/tools/tm-monitor/Makefile index 901b0a14d..a71eb2642 100644 --- a/tools/tm-monitor/Makefile +++ b/tools/tm-monitor/Makefile @@ -36,7 +36,7 @@ dist: build-all build-docker: rm -f ./tm-monitor - docker run -it --rm -v "$(PWD)/../../:/go/src/github.com/tendermint/tendermint" -w "/go/src/github.com/tendermint/tendermint/tools/tm-monitor" -e "CGO_ENABLED=0" golang:alpine go build -ldflags "-s -w" -o tm-monitor + docker run -it --rm -v "$(PWD)/../../:/go/src/github.com/tendermint/tendermint" -w "/go/src/github.com/tendermint/tendermint/tools/tm-monitor" -e "GO111MODULE=on" -e "CGO_ENABLED=0" golang:1.12 go build -ldflags "-s -w" -o tm-monitor docker build -t "tendermint/monitor" . clean: From 78e634dd5c5a193ff56a8ed724e23fd2e0c973d8 Mon Sep 17 00:00:00 2001 From: Karoly Albert Szabo Date: Sat, 13 Jul 2019 08:09:28 +0200 Subject: [PATCH 109/211] tm-monitor: add Context to RPC handlers (#3792) * Fix rpc handle for tm-monitor Signed-off-by: Karoly Albert Szabo * go imports file Signed-off-by: Karoly Albert Szabo * go imports file Signed-off-by: Karoly Albert Szabo * fix RPCUnmonitor too Signed-off-by: Karoly Albert Szabo --- tools/tm-monitor/rpc.go | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tools/tm-monitor/rpc.go b/tools/tm-monitor/rpc.go index 4412e6e0b..42cc23075 100644 --- a/tools/tm-monitor/rpc.go +++ b/tools/tm-monitor/rpc.go @@ -5,9 +5,11 @@ import ( "net" "net/http" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" + "github.com/tendermint/tendermint/libs/log" rpc "github.com/tendermint/tendermint/rpc/lib/server" - monitor "github.com/tendermint/tendermint/tools/tm-monitor/monitor" + "github.com/tendermint/tendermint/tools/tm-monitor/monitor" ) func startRPC(listenAddr string, m *monitor.Monitor, logger log.Logger) net.Listener { @@ -41,33 +43,33 @@ func routes(m *monitor.Monitor) map[string]*rpc.RPCFunc { } // RPCStatus returns common statistics for the network and statistics per node. -func RPCStatus(m *monitor.Monitor) interface{} { - return func() (networkAndNodes, error) { +func RPCStatus(m *monitor.Monitor) func(*rpctypes.Context) (networkAndNodes, error) { + return func(_ *rpctypes.Context) (networkAndNodes, error) { return networkAndNodes{m.Network, m.Nodes}, nil } } // RPCNetworkStatus returns common statistics for the network. -func RPCNetworkStatus(m *monitor.Monitor) interface{} { - return func() (*monitor.Network, error) { +func RPCNetworkStatus(m *monitor.Monitor) func(*rpctypes.Context) (*monitor.Network, error) { + return func(_ *rpctypes.Context) (*monitor.Network, error) { return m.Network, nil } } // RPCNodeStatus returns statistics for the given node. -func RPCNodeStatus(m *monitor.Monitor) interface{} { - return func(name string) (*monitor.Node, error) { +func RPCNodeStatus(m *monitor.Monitor) func(*rpctypes.Context, string) (*monitor.Node, error) { + return func(_ *rpctypes.Context, name string) (*monitor.Node, error) { if i, n := m.NodeByName(name); i != -1 { return n, nil } - return nil, errors.New("Cannot find node with that name") + return nil, errors.New("cannot find node with that name") } } // RPCMonitor allows to dynamically add a endpoint to under the monitor. Safe // to call multiple times. -func RPCMonitor(m *monitor.Monitor) interface{} { - return func(endpoint string) (*monitor.Node, error) { +func RPCMonitor(m *monitor.Monitor) func(*rpctypes.Context, string) (*monitor.Node, error) { + return func(_ *rpctypes.Context, endpoint string) (*monitor.Node, error) { i, n := m.NodeByName(endpoint) if i == -1 { n = monitor.NewNode(endpoint) @@ -80,13 +82,13 @@ func RPCMonitor(m *monitor.Monitor) interface{} { } // RPCUnmonitor removes the given endpoint from under the monitor. -func RPCUnmonitor(m *monitor.Monitor) interface{} { - return func(endpoint string) (bool, error) { +func RPCUnmonitor(m *monitor.Monitor) func(*rpctypes.Context, string) (bool, error) { + return func(_ *rpctypes.Context, endpoint string) (bool, error) { if i, n := m.NodeByName(endpoint); i != -1 { m.Unmonitor(n) return true, nil } - return false, errors.New("Cannot find node with that name") + return false, errors.New("cannot find node with that name") } } From 7a86e49312b8975fd0ce187c5232ebae2eefec6c Mon Sep 17 00:00:00 2001 From: Marko Baricevic Date: Sat, 13 Jul 2019 13:39:11 +0200 Subject: [PATCH 110/211] moved abci change to features as it doesnt break the abci --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24eec219d..930e6e4a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,11 +19,6 @@ program](https://hackerone.com/tendermint). - Go API - - [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127) ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI - client interface (`abcicli.Client`) to allow for supplying the ABCI - `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the - mempool indicate to the ABCI app whether a CheckTx request is a recheck or - not. see [docs](https://github.com/tendermint/tendermint/blob/eddb433d7c082efbeaf8974413a36641519ee895/docs/spec/abci/apps.md#mempool-connection) - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - [libs] [\#2432](https://github.com/tendermint/tendermint/issues/2432) Remove unused `common/heap.go` file (@marbar3778) - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) @@ -33,6 +28,11 @@ program](https://hackerone.com/tendermint). - [node] Add variadic argument to `NewNode` to support functional options, allowing the Node to be more easily customized. - [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass custom reactors to run inside Tendermint node (@ParthDesai) +- [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127) ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI + client interface (`abcicli.Client`) to allow for supplying the ABCI + `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the + mempool indicate to the ABCI app whether a CheckTx request is a recheck or + not. see [docs](https://github.com/tendermint/tendermint/blob/eddb433d7c082efbeaf8974413a36641519ee895/docs/spec/abci/apps.md#mempool-connection) ### IMPROVEMENTS: From 5d1459b5847d65069b776be3b4ba5e81bc933dcc Mon Sep 17 00:00:00 2001 From: Marko Date: Sun, 14 Jul 2019 15:50:33 +0200 Subject: [PATCH 111/211] libs/fail: clean up `fail.go` (#3785) * Clean up `fail.go` - Clean up fail.go - Remove unneeded Func `FailRand()` closes #2729 Signed-off-by: Marko Baricevic * add in commented lines --- CHANGELOG_PENDING.md | 1 + libs/fail/fail.go | 47 +++++++------------------------------------- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d387f9ecb..5131a38ab 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -23,6 +23,7 @@ program](https://hackerone.com/tendermint). - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - [libs] \#2432 Remove unused `common/heap.go` file (@marbar3778) - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) + - [libs] Remove unused `FailRand()` func and minor clean up to `fail.go`(@marbar3778) - Blockchain Protocol diff --git a/libs/fail/fail.go b/libs/fail/fail.go index d7912af5c..0c9220622 100644 --- a/libs/fail/fail.go +++ b/libs/fail/fail.go @@ -2,36 +2,30 @@ package fail import ( "fmt" - "math/rand" "os" "strconv" ) -var callIndexToFail int - -func init() { +func envSet() int { callIndexToFailS := os.Getenv("FAIL_TEST_INDEX") if callIndexToFailS == "" { - callIndexToFail = -1 + return -1 } else { var err error - callIndexToFail, err = strconv.Atoi(callIndexToFailS) + callIndexToFail, err := strconv.Atoi(callIndexToFailS) if err != nil { - callIndexToFail = -1 + return -1 } + return callIndexToFail } } // Fail when FAIL_TEST_INDEX == callIndex -var ( - callIndex int //indexes Fail calls - - callRandIndex int // indexes a run of FailRand calls - callRandIndexToFail = -1 // the callRandIndex to fail on in FailRand -) +var callIndex int //indexes Fail calls func Fail() { + callIndexToFail := envSet() if callIndexToFail < 0 { return } @@ -43,33 +37,6 @@ func Fail() { callIndex += 1 } -// FailRand should be called n successive times. -// It will fail on a random one of those calls -// n must be greater than 0 -func FailRand(n int) { - if callIndexToFail < 0 { - return - } - - if callRandIndexToFail < 0 { - // first call in the loop, pick a random index to fail at - callRandIndexToFail = rand.Intn(n) - callRandIndex = 0 - } - - if callIndex == callIndexToFail { - if callRandIndex == callRandIndexToFail { - Exit() - } - } - - callRandIndex += 1 - - if callRandIndex == n { - callIndex += 1 - } -} - func Exit() { fmt.Printf("*** fail-test %d ***\n", callIndex) os.Exit(1) From e0b9298134aba2297f9d17367ab9364b28c3fe18 Mon Sep 17 00:00:00 2001 From: Marko Date: Sun, 14 Jul 2019 16:02:48 +0200 Subject: [PATCH 112/211] libs: minor cleanup (#3794) * more minor cleanup of libs Remove unused `version.go`, `assert.go` and `libs/circle.yml` * Update types/vote_set_test.go Co-Authored-By: Anton Kaliaev * spelling change --- CHANGELOG_PENDING.md | 1 + libs/circle.yml | 21 --------------------- libs/test/assert.go | 14 -------------- libs/version/version.go | 3 --- types/vote_set_test.go | 5 +++-- types/vote_test.go | 1 + 6 files changed, 5 insertions(+), 40 deletions(-) delete mode 100644 libs/circle.yml delete mode 100644 libs/test/assert.go delete mode 100644 libs/version/version.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 5131a38ab..0120b03ce 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -23,6 +23,7 @@ program](https://hackerone.com/tendermint). - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - [libs] \#2432 Remove unused `common/heap.go` file (@marbar3778) - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) + - [libs] Remove unused `version.go`, `assert.go` and `libs/circle.yml` - [libs] Remove unused `FailRand()` func and minor clean up to `fail.go`(@marbar3778) - Blockchain Protocol diff --git a/libs/circle.yml b/libs/circle.yml deleted file mode 100644 index 2b7d1266c..000000000 --- a/libs/circle.yml +++ /dev/null @@ -1,21 +0,0 @@ -machine: - environment: - GOPATH: "${HOME}/.go_workspace" - PROJECT_PARENT_PATH: "$GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME" - PROJECT_PATH: $GOPATH/src/github.com/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME - hosts: - localhost: 127.0.0.1 - -dependencies: - override: - - mkdir -p "$PROJECT_PARENT_PATH" - - ln -sf "$HOME/$CIRCLE_PROJECT_REPONAME/" "$PROJECT_PATH" - post: - - go version - -test: - override: - - cd $PROJECT_PATH && make get_tools && bash ./test.sh - post: - - cd "$PROJECT_PATH" && bash <(curl -s https://codecov.io/bash) -f coverage.txt - - cd "$PROJECT_PATH" && mv coverage.txt "${CIRCLE_ARTIFACTS}" diff --git a/libs/test/assert.go b/libs/test/assert.go deleted file mode 100644 index a6ffed0ce..000000000 --- a/libs/test/assert.go +++ /dev/null @@ -1,14 +0,0 @@ -package test - -import ( - "testing" -) - -func AssertPanics(t *testing.T, msg string, f func()) { - defer func() { - if err := recover(); err == nil { - t.Errorf("Should have panic'd, but didn't: %v", msg) - } - }() - f() -} diff --git a/libs/version/version.go b/libs/version/version.go deleted file mode 100644 index 6e73a937d..000000000 --- a/libs/version/version.go +++ /dev/null @@ -1,3 +0,0 @@ -package version - -const Version = "0.9.0" diff --git a/types/vote_set_test.go b/types/vote_set_test.go index 59205efc6..2e217e940 100644 --- a/types/vote_set_test.go +++ b/types/vote_set_test.go @@ -4,9 +4,10 @@ import ( "bytes" "testing" + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/crypto" cmn "github.com/tendermint/tendermint/libs/common" - tst "github.com/tendermint/tendermint/libs/test" tmtime "github.com/tendermint/tendermint/types/time" ) @@ -490,7 +491,7 @@ func TestMakeCommit(t *testing.T) { } // MakeCommit should fail. - tst.AssertPanics(t, "Doesn't have +2/3 majority", func() { voteSet.MakeCommit() }) + assert.Panics(t, func() { voteSet.MakeCommit() }, "Doesn't have +2/3 majority") // 7th voted for some other block. { diff --git a/types/vote_test.go b/types/vote_test.go index af8a9625b..b6eb1f586 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -7,6 +7,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + amino "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" From 86f70893969f8399ccc8a7326544696e6040d856 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 15 Jul 2019 19:31:35 +0400 Subject: [PATCH 113/211] add a changelog entry (#3802) Refs https://github.com/tendermint/tendermint/pull/3758#issuecomment-510738955 --- CHANGELOG_PENDING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 0120b03ce..89eccbc6e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -40,6 +40,7 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: - [rpc] \#3700 Make possible to set absolute paths for TLS cert and key (@climber73) +- [abci] [\#3513](https://github.com/tendermint/tendermint/issues/3513) Call the reqRes callback after the resCb so they always happen in the same order ### BUG FIXES: From 245e1c9ef7608fcc3364ea958627da97ea0351a1 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 15 Jul 2019 17:31:50 +0200 Subject: [PATCH 114/211] pr comments (#3803) * pr comments * abci changelog change --- CHANGELOG.md | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b9d11f58f..74ca9cd31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,14 @@ ## v0.32.1 -*July 12, 2019* +*July 15, 2019* Special thanks to external contributors on this release: @ParthDesai, @climber73, @jim380, @ashleyvega -This release contains breaking changes to our libs folder and ABCI namely: +This release contains a minor enhancement to the ABCI and some breaking changes to our libs folder, namely: - CheckTx requests include a `CheckTxType` enum that can be set to `Recheck` to indicate to the application that this transaction was already checked/validated and certain expensive operations (like checking signatures) can be skipped - Removed various functions from `libs` pkgs - `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` Friendly reminder, we have a [bug bounty program](https://hackerone.com/tendermint). @@ -19,6 +18,7 @@ program](https://hackerone.com/tendermint). - Go API + - [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127) The CheckTx and DeliverTx methods in the ABCI `Application` interface now take structs as arguments (RequestCheckTx and RequestDeliverTx, respectively), instead of just the raw tx bytes. This allows more information to be passed to these methods, for instance, indicating whether a tx has already been checked. - [libs] Remove unused `db/debugDB` and `common/colors.go` & `errors/errors.go` files (@marbar3778) - [libs] [\#2432](https://github.com/tendermint/tendermint/issues/2432) Remove unused `common/heap.go` file (@marbar3778) - [libs] Remove unused `date.go`, `io.go`. Remove `GoPath()`, `Prompt()` and `IsDirEmpty()` functions from `os.go` (@marbar3778) @@ -29,11 +29,7 @@ program](https://hackerone.com/tendermint). - [node] Add variadic argument to `NewNode` to support functional options, allowing the Node to be more easily customized. - [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass custom reactors to run inside Tendermint node (@ParthDesai) -- [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127) ABCI / mempool: Add a "Recheck Tx" indicator. Breaks the ABCI - client interface (`abcicli.Client`) to allow for supplying the ABCI - `types.RequestCheckTx` and `types.RequestDeliverTx` structs, and lets the - mempool indicate to the ABCI app whether a CheckTx request is a recheck or - not. see [docs](https://github.com/tendermint/tendermint/blob/eddb433d7c082efbeaf8974413a36641519ee895/docs/spec/abci/apps.md#mempool-connection) +- [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127)RequestCheckTx has a new field, `CheckTxType`, which can take values of `CheckTxType_New` and `CheckTxType_Recheck`, indicating whether this is a new tx being checked for the first time or whether this tx is being rechecked after a block commit. This allows applications to skip certain expensive operations, like signature checking, if they've already been done once. see [docs](https://github.com/tendermint/tendermint/blob/eddb433d7c082efbeaf8974413a36641519ee895/docs/spec/abci/apps.md#mempool-connection) ### IMPROVEMENTS: From 0c9a284f8df898a80e4c26052f961b3158e72507 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 15 Jul 2019 17:42:08 +0200 Subject: [PATCH 115/211] Marko/update release1 (#3806) * pr comments * remove empty space --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ca9cd31..cf2879c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: - [rpc] [\#3700](https://github.com/tendermint/tendermint/issues/3700) Make possible to set absolute paths for TLS cert and key (@climber73) +- [abci] [\#3513](https://github.com/tendermint/tendermint/issues/3513) Call the reqRes callback after the resCb so they always happen in the same order ### BUG FIXES: From e2775ba0e3dccad48831b0a136115cf4bfce37b2 Mon Sep 17 00:00:00 2001 From: Roman Useinov Date: Wed, 17 Jul 2019 06:37:27 +0200 Subject: [PATCH 116/211] abci/server: recover from app panics in socket server (#3809) fixes #3800 --- CHANGELOG_PENDING.md | 1 + abci/server/socket_server.go | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f211a27f3..5dbc1ddb7 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -17,5 +17,6 @@ ### FEATURES: ### IMPROVEMENTS: +- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) ### BUG FIXES: diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 4b92f04cf..38649ac7f 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -146,6 +146,16 @@ func (s *SocketServer) waitForClose(closeConn chan error, connID int) { func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, responses chan<- *types.Response) { var count int var bufReader = bufio.NewReader(conn) + + defer func() { + // make sure to recover from any app-related panics to allow proper socket cleanup + r := recover() + if r != nil { + closeConn <- fmt.Errorf("recovered from panic: %v", r) + s.appMtx.Unlock() + } + }() + for { var req = &types.Request{} @@ -154,7 +164,7 @@ func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, respo if err == io.EOF { closeConn <- err } else { - closeConn <- fmt.Errorf("Error reading message: %v", err.Error()) + closeConn <- fmt.Errorf("error reading message: %v", err) } return } From 7924c7681528d7f95229d72c154c8cd35543577d Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 2 Jul 2019 21:19:58 +0400 Subject: [PATCH 117/211] p2p: dial addrs which came from seed instead of calling ensurePeers (#3762) Calling ensurePeers outside of ensurePeersRoutine can lead to nodes disconnecting from us due to "sent next PEX request too soon" error. Solution is to just dial addrs we got from src instead of calling ensurePeers. Refs #2093 Fixes #3338 --- CHANGELOG_PENDING.md | 2 ++ p2p/pex/pex_reactor.go | 29 ++++++++++++++++++++++++----- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 5dbc1ddb7..d913443a2 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,3 +20,5 @@ - [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) ### BUG FIXES: +- [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling + ensurePeers outside of ensurePeersRoutine diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index eabbc4d61..e0d7c0d72 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -340,6 +340,15 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { if err != nil { return err } + + srcIsSeed := false + for _, seedAddr := range r.seedAddrs { + if seedAddr.Equals(srcAddr) { + srcIsSeed = true + break + } + } + for _, netAddr := range addrs { // Validate netAddr. Disconnect from a peer if it sends us invalid data. if netAddr == nil { @@ -365,13 +374,23 @@ func (r *PEXReactor) ReceiveAddrs(addrs []*p2p.NetAddress, src Peer) error { } // If this address came from a seed node, try to connect to it without - // waiting. - for _, seedAddr := range r.seedAddrs { - if seedAddr.Equals(srcAddr) { - r.ensurePeers() - } + // waiting (#2093) + if srcIsSeed { + r.Logger.Info("Will dial address, which came from seed", "addr", netAddr, "seed", srcAddr) + go func(addr *p2p.NetAddress) { + err := r.dialPeer(addr) + if err != nil { + switch err.(type) { + case errMaxAttemptsToDial, errTooEarlyToDial: + r.Logger.Debug(err.Error(), "addr", addr) + default: + r.Logger.Error(err.Error(), "addr", addr) + } + } + }(netAddr) } } + return nil } From bb9ee2ca2818a687ef7860ff3b893865ad65c49c Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 29 Jul 2019 17:58:58 +0400 Subject: [PATCH 118/211] bump version and update changelog --- CHANGELOG.md | 12 ++++++++++++ CHANGELOG_PENDING.md | 3 --- version/version.go | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 068d99c3c..5bfcf36cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +*July 29, 2019* + +This releases fixes one bug in the PEX reactor and adds a `recover` to the Go's +ABCI server, which allows it to properly cleanup. + +### IMPROVEMENTS: +- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) + +### BUG FIXES: +- [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling + ensurePeers outside of ensurePeersRoutine + ## v0.31.7 *June 3, 2019* diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index d913443a2..f211a27f3 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -17,8 +17,5 @@ ### FEATURES: ### IMPROVEMENTS: -- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) ### BUG FIXES: -- [p2p] \#3338 Prevent "sent next PEX request too soon" errors by not calling - ensurePeers outside of ensurePeersRoutine diff --git a/version/version.go b/version/version.go index 1a15717f7..b0d4ff1a5 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.31.7" + TMCoreSemVer = "0.31.8" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.0" From 9df117748eaf712afed3757204ddd9580025e8cb Mon Sep 17 00:00:00 2001 From: Alex Dupre Date: Mon, 15 Jul 2019 21:04:06 +0200 Subject: [PATCH 119/211] docs: fix consensus spec formatting (#3804) --- docs/spec/consensus/consensus.md | 50 ++++++++++++++++++-------------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/docs/spec/consensus/consensus.md b/docs/spec/consensus/consensus.md index ec6659c96..7b424dc6b 100644 --- a/docs/spec/consensus/consensus.md +++ b/docs/spec/consensus/consensus.md @@ -73,11 +73,11 @@ parameters over each successive round. |(When +2/3 Precommits for block found) | v | +--------------------------------------------------------------------+ - | Commit | - | | - | * Set CommitTime = now; | - | * Wait for block, then stage/save/commit block; | - +--------------------------------------------------------------------+ +| Commit | +| | +| * Set CommitTime = now; | +| * Wait for block, then stage/save/commit block; | ++--------------------------------------------------------------------+ ``` # Background Gossip @@ -131,13 +131,15 @@ liveness property. ### Propose Step (height:H,round:R) -Upon entering `Propose`: - The designated proposer proposes a block at -`(H,R)`. +Upon entering `Propose`: +- The designated proposer proposes a block at `(H,R)`. -The `Propose` step ends: - After `timeoutProposeR` after entering -`Propose`. --> goto `Prevote(H,R)` - After receiving proposal block -and all prevotes at `PoLC-Round`. --> goto `Prevote(H,R)` - After -[common exit conditions](#common-exit-conditions) +The `Propose` step ends: +- After `timeoutProposeR` after entering `Propose`. --> goto + `Prevote(H,R)` +- After receiving proposal block and all prevotes at `PoLC-Round`. --> + goto `Prevote(H,R)` +- After [common exit conditions](#common-exit-conditions) ### Prevote Step (height:H,round:R) @@ -152,10 +154,12 @@ Upon entering `Prevote`, each validator broadcasts its prevote vote. - Else, if the proposal is invalid or wasn't received on time, it prevotes ``. -The `Prevote` step ends: - After +2/3 prevotes for a particular block or -``. -->; goto `Precommit(H,R)` - After `timeoutPrevote` after -receiving any +2/3 prevotes. --> goto `Precommit(H,R)` - After -[common exit conditions](#common-exit-conditions) +The `Prevote` step ends: +- After +2/3 prevotes for a particular block or ``. -->; goto + `Precommit(H,R)` +- After `timeoutPrevote` after receiving any +2/3 prevotes. --> goto + `Precommit(H,R)` +- After [common exit conditions](#common-exit-conditions) ### Precommit Step (height:H,round:R) @@ -163,17 +167,19 @@ Upon entering `Precommit`, each validator broadcasts its precommit vote. - If the validator has a PoLC at `(H,R)` for a particular block `B`, it (re)locks (or changes lock to) and precommits `B` and sets - `LastLockRound = R`. - Else, if the validator has a PoLC at `(H,R)` for - ``, it unlocks and precommits ``. - Else, it keeps the lock - unchanged and precommits ``. + `LastLockRound = R`. +- Else, if the validator has a PoLC at `(H,R)` for ``, it unlocks + and precommits ``. +- Else, it keeps the lock unchanged and precommits ``. A precommit for `` means "I didn’t see a PoLC for this round, but I did get +2/3 prevotes and waited a bit". -The Precommit step ends: - After +2/3 precommits for ``. --> -goto `Propose(H,R+1)` - After `timeoutPrecommit` after receiving any -+2/3 precommits. --> goto `Propose(H,R+1)` - After [common exit -conditions](#common-exit-conditions) +The Precommit step ends: +- After +2/3 precommits for ``. --> goto `Propose(H,R+1)` +- After `timeoutPrecommit` after receiving any +2/3 precommits. --> goto + `Propose(H,R+1)` +- After [common exit conditions](#common-exit-conditions) ### Common exit conditions From 9867a65de7f912498636ad58f2d1f29912fe79dd Mon Sep 17 00:00:00 2001 From: Roman Useinov Date: Wed, 17 Jul 2019 06:37:27 +0200 Subject: [PATCH 120/211] abci/server: recover from app panics in socket server (#3809) fixes #3800 --- CHANGELOG_PENDING.md | 1 + abci/server/socket_server.go | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index fd5c4b27e..41400d892 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -18,5 +18,6 @@ program](https://hackerone.com/tendermint). ### FEATURES: ### IMPROVEMENTS: +- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) ### BUG FIXES: diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 96cb844b7..82ce610ed 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -146,6 +146,16 @@ func (s *SocketServer) waitForClose(closeConn chan error, connID int) { func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, responses chan<- *types.Response) { var count int var bufReader = bufio.NewReader(conn) + + defer func() { + // make sure to recover from any app-related panics to allow proper socket cleanup + r := recover() + if r != nil { + closeConn <- fmt.Errorf("recovered from panic: %v", r) + s.appMtx.Unlock() + } + }() + for { var req = &types.Request{} @@ -154,7 +164,7 @@ func (s *SocketServer) handleRequests(closeConn chan error, conn net.Conn, respo if err == io.EOF { closeConn <- err } else { - closeConn <- fmt.Errorf("Error reading message: %v", err.Error()) + closeConn <- fmt.Errorf("error reading message: %v", err) } return } From 8da43508f8405ea77db51ece2615f5f9338002b6 Mon Sep 17 00:00:00 2001 From: Marko Date: Wed, 17 Jul 2019 09:49:01 +0200 Subject: [PATCH 121/211] abci/client: fix DATA RACE in gRPC client (#3798) * Remove go func {}() closes #357 - Remove go func(){}() that caused race condiditon - To reproduce - add -race in make file to `install_abci` - Remove `CGO_ENABLED=0` & add -race to `install` Signed-off-by: Marko Baricevic * remove -race * fix data race also, reorder callbacks similarly to socket client --- abci/client/grpc_client.go | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index 23d790550..8c444abc5 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -228,18 +228,22 @@ func (cli *grpcClient) finishAsyncCall(req *types.Request, res *types.Response) reqres.Done() // Release waiters reqres.SetDone() // so reqRes.SetCallback will run the callback - // go routine for callbacks + // goroutine for callbacks go func() { - // Notify reqRes listener if set - if cb := reqres.GetCallback(); cb != nil { - cb(res) - } + cli.mtx.Lock() + defer cli.mtx.Unlock() // Notify client listener if set if cli.resCb != nil { cli.resCb(reqres.Request, res) } + + // Notify reqRes listener if set + if cb := reqres.GetCallback(); cb != nil { + cb(res) + } }() + return reqres } From c264db339e526658a01375da10dbb14b013ce3de Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 18 Jul 2019 15:15:14 +0400 Subject: [PATCH 122/211] docs: "Writing a built-in Tendermint Core application in Go" guide (#3608) * docs: go built-in guide * fix package imports, add badger db, simplify Query * newTendermint function * working example * finish the first guide * add one more note * add the second Golang guide - external ABCI app * fix typos --- docs/guides/go-built-in.md | 630 +++++++++++++++++++++++++++++++++++++ docs/guides/go.md | 514 ++++++++++++++++++++++++++++++ 2 files changed, 1144 insertions(+) create mode 100644 docs/guides/go-built-in.md create mode 100644 docs/guides/go.md diff --git a/docs/guides/go-built-in.md b/docs/guides/go-built-in.md new file mode 100644 index 000000000..a0c76c9e9 --- /dev/null +++ b/docs/guides/go-built-in.md @@ -0,0 +1,630 @@ +# 1 Guide Assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine - written in any programming language - and securely +replicates it on many machines. + +Although Tendermint Core is written in the Golang programming language, prior +knowledge of it is not required for this guide. You can learn it as we go due +to it's simplicity. However, you may want to go through [Learn X in Y minutes +Where X=Go](https://learnxinyminutes.com/docs/go/) first to familiarize +yourself with the syntax. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. + +# 1 Creating a built-in application in Go + +Running your application inside the same process as Tendermint Core will give +you the best possible performance. + +For other languages, your application have to communicate with Tendermint Core +through a TCP, Unix domain socket or gRPC. + +## 1.1 Installing Go + +Please refer to [the official guide for installing +Go](https://golang.org/doc/install). + +Verify that you have the latest version of Go installed: + +```sh +$ go version +go version go1.12.7 darwin/amd64 +``` + +Make sure you have `$GOPATH` environment variable set: + +```sh +$ echo $GOPATH +/Users/melekes/go +``` + +## 1.2 Creating a new Go project + +We'll start by creating a new Go project. + +```sh +$ mkdir -p $GOPATH/src/github.com/me/kvstore +$ cd $GOPATH/src/github.com/me/kvstore +``` + +Inside the example directory create a `main.go` file with the following content: + +```go +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, Tendermint Core") +} +``` + +When run, this should print "Hello, Tendermint Core" to the standard output. + +```sh +$ go run main.go +Hello, Tendermint Core +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +Create a file called `app.go` with the following content: + +```go +package main + +import ( + abcitypes "github.com/tendermint/tendermint/abci/types" +) + +type KVStoreApplication struct {} + +var _ abcitypes.Application = (*KVStoreApplication)(nil) + +func NewKVStoreApplication() *KVStoreApplication { + return &KVStoreApplication{} +} + +func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo { + return abcitypes.ResponseInfo{} +} + +func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption { + return abcitypes.ResponseSetOption{} +} + +func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + return abcitypes.ResponseDeliverTx{Code: 0} +} + +func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + return abcitypes.ResponseCheckTx{Code: 0} +} + +func (KVStoreApplication) Commit() abcitypes.ResponseCommit { + return abcitypes.ResponseCommit{} +} + +func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { + return abcitypes.ResponseQuery{Code: 0} +} + +func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { + return abcitypes.ResponseInitChain{} +} + +func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + return abcitypes.ResponseBeginBlock{} +} + +func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { + return abcitypes.ResponseEndBlock{} +} +``` + +Now I will go through each method explaining when it's called and adding +required business logic. + +### 1.3.1 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```go +func (app *KVStoreApplication) isValid(tx []byte) (code uint32) { + // check format + parts := bytes.Split(tx, []byte("=")) + if len(parts) != 2 { + return 1 + } + + key, value := parts[0], parts[1] + + // check if the same key=value already exists + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(key) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == nil { + return item.Value(func(val []byte) error { + if bytes.Equal(val, value) { + code = 2 + } + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + + return code +} + +func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + code := app.isValid(req.Tx) + return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1} +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[badger](https://github.com/dgraph-io/badger), which is an embeddable, +persistent and fast key-value (KV) database. + +```go +import "github.com/dgraph-io/badger" + +type KVStoreApplication struct { + db *badger.DB + currentBatch *badger.Txn +} + +func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { + return &KVStoreApplication{ + db: db, + } +} +``` + +### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transfered to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. DeliverTx are being transfered asynchronously, but the +responses are expected to come in order. + +``` +func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + app.currentBatch = app.db.NewTransaction(true) + return abcitypes.ResponseBeginBlock{} +} + +``` + +Here we create a batch, which will store block's transactions. + +```go +func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + code := app.isValid(req.Tx) + if code != 0 { + return abcitypes.ResponseDeliverTx{Code: code} + } + + parts := bytes.Split(req.Tx, []byte("=")) + key, value := parts[0], parts[1] + + err := app.currentBatch.Set(key, value) + if err != nil { + panic(err) + } + + return abcitypes.ResponseDeliverTx{Code: 0} +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the current batch. + +In the current design, a block can include incorrect transactions (those who +passed CheckTx, but failed DeliverTx or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```go +func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { + app.currentBatch.Commit() + return abcitypes.ResponseCommit{Data: []byte{}} +} +``` + +### 1.3.3 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```go +func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) { + resQuery.Key = reqQuery.Data + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(reqQuery.Data) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == badger.ErrKeyNotFound { + resQuery.Log = "does not exist" + } else { + return item.Value(func(val []byte) error { + resQuery.Log = "exists" + resQuery.Value = val + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + return +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instance in the same process + +Put the following code into the "main.go" file: + +```go +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "path/filepath" + "syscall" + + "github.com/dgraph-io/badger" + "github.com/pkg/errors" + "github.com/spf13/viper" + + abci "github.com/tendermint/tendermint/abci/types" + cfg "github.com/tendermint/tendermint/config" + tmflags "github.com/tendermint/tendermint/libs/cli/flags" + "github.com/tendermint/tendermint/libs/log" + nm "github.com/tendermint/tendermint/node" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/proxy" +) + +var configFile string + +func init() { + flag.StringVar(&configFile, "config", "$HOME/.tendermint/config/config.toml", "Path to config.toml") +} + +func main() { + db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) + } + defer db.Close() + app := NewKVStoreApplication(db) + + flag.Parse() + + node, err := newTendermint(app, configFile) + if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(2) + } + + node.Start() + defer func() { + node.Stop() + node.Wait() + }() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + os.Exit(0) +} + +func newTendermint(app abci.Application, configFile string) (*nm.Node, error) { + // read config + config := cfg.DefaultConfig() + config.RootDir = filepath.Dir(filepath.Dir(configFile)) + viper.SetConfigFile(configFile) + if err := viper.ReadInConfig(); err != nil { + return nil, errors.Wrap(err, "viper failed to read config file") + } + if err := viper.Unmarshal(config); err != nil { + return nil, errors.Wrap(err, "viper failed to unmarshal config") + } + if err := config.ValidateBasic(); err != nil { + return nil, errors.Wrap(err, "config is invalid") + } + + // create logger + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + var err error + logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel()) + if err != nil { + return nil, errors.Wrap(err, "failed to parse log level") + } + + // read private validator + pv := privval.LoadFilePV( + config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile(), + ) + + // read node key + nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) + if err != nil { + return nil, errors.Wrap(err, "failed to load node's key") + } + + // create node + node, err := nm.NewNode( + config, + pv, + nodeKey, + proxy.NewLocalClientCreator(app), + nm.DefaultGenesisDocProviderFunc(config), + nm.DefaultDBProvider, + nm.DefaultMetricsProvider(config.Instrumentation), + logger) + if err != nil { + return nil, errors.Wrap(err, "failed to create new Tendermint node") + } + + return node, nil +} +``` + +This is a huge blob of code, so let's break it down into pieces. + +First, we initialize the Badger database and create an app instance: + +```go +db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) +if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) +} +defer db.Close() +app := NewKVStoreApplication(db) +``` + +Then we use it to create a Tendermint Core `Node` instance: + +```go +flag.Parse() + +node, err := newTendermint(app, configFile) +if err != nil { + fmt.Fprintf(os.Stderr, "%v", err) + os.Exit(2) +} + +... + +// create node +node, err := nm.NewNode( + config, + pv, + nodeKey, + proxy.NewLocalClientCreator(app), + nm.DefaultGenesisDocProviderFunc(config), + nm.DefaultDBProvider, + nm.DefaultMetricsProvider(config.Instrumentation), + logger) +if err != nil { + return nil, errors.Wrap(err, "failed to create new Tendermint node") +} +``` + +`NewNode` requires a few things including a configuration file, a private +validator, a node key and a few others in order to construct the full node. + +Note we use `proxy.NewLocalClientCreator` here to create a local client instead +of one communicating through a socket or gRPC. + +[viper](https://github.com/spf13/viper) is being used for reading the config, +which we will generate later using the `tendermint init` command. + +```go +config := cfg.DefaultConfig() +config.RootDir = filepath.Dir(filepath.Dir(configFile)) +viper.SetConfigFile(configFile) +if err := viper.ReadInConfig(); err != nil { + return nil, errors.Wrap(err, "viper failed to read config file") +} +if err := viper.Unmarshal(config); err != nil { + return nil, errors.Wrap(err, "viper failed to unmarshal config") +} +if err := config.ValidateBasic(); err != nil { + return nil, errors.Wrap(err, "config is invalid") +} +``` + +We use `FilePV`, which is a private validator (i.e. thing which signs consensus +messages). Normally, you would use `SignerRemote` to connect to an external +[HSM](https://kb.certus.one/hsm.html). + +```go +pv := privval.LoadFilePV( + config.PrivValidatorKeyFile(), + config.PrivValidatorStateFile(), +) + +``` + +`nodeKey` is needed to identify the node in a p2p network. + +```go +nodeKey, err := p2p.LoadNodeKey(config.NodeKeyFile()) +if err != nil { + return nil, errors.Wrap(err, "failed to load node's key") +} +``` + +As for the logger, we use the build-in library, which provides a nice +abstraction over [go-kit's +logger](https://github.com/go-kit/kit/tree/master/log). + +```go +logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) +var err error +logger, err = tmflags.ParseLogLevel(config.LogLevel, logger, cfg.DefaultLogLevel()) +if err != nil { + return nil, errors.Wrap(err, "failed to parse log level") +} +``` + +Finally, we start the node and add some signal handling to gracefully stop it +upon receiving SIGTERM or Ctrl-C. + +```go +node.Start() +defer func() { + node.Stop() + node.Wait() +}() + +c := make(chan os.Signal, 1) +signal.Notify(c, os.Interrupt, syscall.SIGTERM) +<-c +os.Exit(0) +``` + +## 1.5 Getting Up and Running + +We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for +dependency management. + +```sh +$ export GO111MODULE=on +$ go mod init github.com/me/example +$ go build +``` + +This should build the binary. + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:40:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:40:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:40:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +We are ready to start our application: + +```sh +$ ./example -config "/tmp/example/config/config.toml" + +badger 2019/07/16 18:42:25 INFO: All 0 tables opened in 0s +badger 2019/07/16 18:42:25 INFO: Replaying file id: 0 at offset: 0 +badger 2019/07/16 18:42:25 INFO: Replay took: 695.227s +E[2019-07-16|18:42:25.818] Couldn't connect to any seeds module=p2p +I[2019-07-16|18:42:26.853] Executed block module=state height=1 validTxs=0 invalidTxs=0 +I[2019-07-16|18:42:26.865] Committed state module=state height=1 txs=0 appHash= +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "1B3C5A1093DB952C331B1749A21DCCBB0F6C7F4E0055CD04D16346472FC60EC6", + "height": "128" + } +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +``` +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3M=" + } + } +} +``` + +"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of +"tendermint" and "rocks" accordingly. diff --git a/docs/guides/go.md b/docs/guides/go.md new file mode 100644 index 000000000..abda07955 --- /dev/null +++ b/docs/guides/go.md @@ -0,0 +1,514 @@ +# 1 Guide Assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine - written in any programming language - and securely +replicates it on many machines. + +Although Tendermint Core is written in the Golang programming language, prior +knowledge of it is not required for this guide. You can learn it as we go due +to it's simplicity. However, you may want to go through [Learn X in Y minutes +Where X=Go](https://learnxinyminutes.com/docs/go/) first to familiarize +yourself with the syntax. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. + +# 1 Creating an application in Go + +To get maximum performance it is better to run your application alongside +Tendermint Core. [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written +this way. Please refer to [Writing a built-in Tendermint Core application in +Go](./go-built-in.md) guide for details. + +Having a separate application might give you better security guarantees as two +processes would be communicating via established binary protocol. Tendermint +Core will not have access to application's state. + +## 1.1 Installing Go + +Please refer to [the official guide for installing +Go](https://golang.org/doc/install). + +Verify that you have the latest version of Go installed: + +```sh +$ go version +go version go1.12.7 darwin/amd64 +``` + +Make sure you have `$GOPATH` environment variable set: + +```sh +$ echo $GOPATH +/Users/melekes/go +``` + +## 1.2 Creating a new Go project + +We'll start by creating a new Go project. + +```sh +$ mkdir -p $GOPATH/src/github.com/me/kvstore +$ cd $GOPATH/src/github.com/me/kvstore +``` + +Inside the example directory create a `main.go` file with the following content: + +```go +package main + +import ( + "fmt" +) + +func main() { + fmt.Println("Hello, Tendermint Core") +} +``` + +When run, this should print "Hello, Tendermint Core" to the standard output. + +```sh +$ go run main.go +Hello, Tendermint Core +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +Create a file called `app.go` with the following content: + +```go +package main + +import ( + abcitypes "github.com/tendermint/tendermint/abci/types" +) + +type KVStoreApplication struct {} + +var _ abcitypes.Application = (*KVStoreApplication)(nil) + +func NewKVStoreApplication() *KVStoreApplication { + return &KVStoreApplication{} +} + +func (KVStoreApplication) Info(req abcitypes.RequestInfo) abcitypes.ResponseInfo { + return abcitypes.ResponseInfo{} +} + +func (KVStoreApplication) SetOption(req abcitypes.RequestSetOption) abcitypes.ResponseSetOption { + return abcitypes.ResponseSetOption{} +} + +func (KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + return abcitypes.ResponseDeliverTx{Code: 0} +} + +func (KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + return abcitypes.ResponseCheckTx{Code: 0} +} + +func (KVStoreApplication) Commit() abcitypes.ResponseCommit { + return abcitypes.ResponseCommit{} +} + +func (KVStoreApplication) Query(req abcitypes.RequestQuery) abcitypes.ResponseQuery { + return abcitypes.ResponseQuery{Code: 0} +} + +func (KVStoreApplication) InitChain(req abcitypes.RequestInitChain) abcitypes.ResponseInitChain { + return abcitypes.ResponseInitChain{} +} + +func (KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + return abcitypes.ResponseBeginBlock{} +} + +func (KVStoreApplication) EndBlock(req abcitypes.RequestEndBlock) abcitypes.ResponseEndBlock { + return abcitypes.ResponseEndBlock{} +} +``` + +Now I will go through each method explaining when it's called and adding +required business logic. + +### 1.3.1 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```go +func (app *KVStoreApplication) isValid(tx []byte) (code uint32) { + // check format + parts := bytes.Split(tx, []byte("=")) + if len(parts) != 2 { + return 1 + } + + key, value := parts[0], parts[1] + + // check if the same key=value already exists + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(key) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == nil { + return item.Value(func(val []byte) error { + if bytes.Equal(val, value) { + code = 2 + } + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + + return code +} + +func (app *KVStoreApplication) CheckTx(req abcitypes.RequestCheckTx) abcitypes.ResponseCheckTx { + code := app.isValid(req.Tx) + return abcitypes.ResponseCheckTx{Code: code, GasWanted: 1} +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[badger](https://github.com/dgraph-io/badger), which is an embeddable, +persistent and fast key-value (KV) database. + +```go +import "github.com/dgraph-io/badger" + +type KVStoreApplication struct { + db *badger.DB + currentBatch *badger.Txn +} + +func NewKVStoreApplication(db *badger.DB) *KVStoreApplication { + return &KVStoreApplication{ + db: db, + } +} +``` + +### 1.3.2 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transfered to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. DeliverTx are being transfered asynchronously, but the +responses are expected to come in order. + +``` +func (app *KVStoreApplication) BeginBlock(req abcitypes.RequestBeginBlock) abcitypes.ResponseBeginBlock { + app.currentBatch = app.db.NewTransaction(true) + return abcitypes.ResponseBeginBlock{} +} + +``` + +Here we create a batch, which will store block's transactions. + +```go +func (app *KVStoreApplication) DeliverTx(req abcitypes.RequestDeliverTx) abcitypes.ResponseDeliverTx { + code := app.isValid(req.Tx) + if code != 0 { + return abcitypes.ResponseDeliverTx{Code: code} + } + + parts := bytes.Split(req.Tx, []byte("=")) + key, value := parts[0], parts[1] + + err := app.currentBatch.Set(key, value) + if err != nil { + panic(err) + } + + return abcitypes.ResponseDeliverTx{Code: 0} +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the current batch. + +In the current design, a block can include incorrect transactions (those who +passed CheckTx, but failed DeliverTx or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```go +func (app *KVStoreApplication) Commit() abcitypes.ResponseCommit { + app.currentBatch.Commit() + return abcitypes.ResponseCommit{Data: []byte{}} +} +``` + +### 1.3.3 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```go +func (app *KVStoreApplication) Query(reqQuery abcitypes.RequestQuery) (resQuery abcitypes.ResponseQuery) { + resQuery.Key = reqQuery.Data + err := app.db.View(func(txn *badger.Txn) error { + item, err := txn.Get(reqQuery.Data) + if err != nil && err != badger.ErrKeyNotFound { + return err + } + if err == badger.ErrKeyNotFound { + resQuery.Log = "does not exist" + } else { + return item.Value(func(val []byte) error { + resQuery.Log = "exists" + resQuery.Value = val + return nil + }) + } + return nil + }) + if err != nil { + panic(err) + } + return +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instances + +Put the following code into the "main.go" file: + +```go +package main + +import ( + "flag" + "fmt" + "os" + "os/signal" + "syscall" + + "github.com/dgraph-io/badger" + + abciserver "github.com/tendermint/tendermint/abci/server" + "github.com/tendermint/tendermint/libs/log" +) + +var socketAddr string + +func init() { + flag.StringVar(&socketAddr, "socket-addr", "unix://example.sock", "Unix domain socket address") +} + +func main() { + db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) + } + defer db.Close() + app := NewKVStoreApplication(db) + + flag.Parse() + + logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) + + server := abciserver.NewSocketServer(socketAddr, app) + server.SetLogger(logger) + if err := server.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) + os.Exit(1) + } + defer server.Stop() + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + <-c + os.Exit(0) +} +``` + +This is a huge blob of code, so let's break it down into pieces. + +First, we initialize the Badger database and create an app instance: + +```go +db, err := badger.Open(badger.DefaultOptions("/tmp/badger")) +if err != nil { + fmt.Fprintf(os.Stderr, "failed to open badger db: %v", err) + os.Exit(1) +} +defer db.Close() +app := NewKVStoreApplication(db) +``` + +Then we start the ABCI server and add some signal handling to gracefully stop +it upon receiving SIGTERM or Ctrl-C. Tendermint Core will act as a client, +which connects to our server and send us transactions and other messages. + +```go +server := abciserver.NewSocketServer(socketAddr, app) +server.SetLogger(logger) +if err := server.Start(); err != nil { + fmt.Fprintf(os.Stderr, "error starting socket server: %v", err) + os.Exit(1) +} +defer server.Stop() + +c := make(chan os.Signal, 1) +signal.Notify(c, os.Interrupt, syscall.SIGTERM) +<-c +os.Exit(0) +``` + +## 1.5 Getting Up and Running + +We are going to use [Go modules](https://github.com/golang/go/wiki/Modules) for +dependency management. + +```sh +$ export GO111MODULE=on +$ go mod init github.com/me/example +$ go build +``` + +This should build the binary. + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +Feel free to explore the generated files, which can be found at +`/tmp/example/config` directory. Documentation on the config can be found +[here](https://tendermint.com/docs/tendermint-core/configuration.html). + +We are ready to start our application: + +```sh +$ rm example.sock +$ ./example + +badger 2019/07/16 18:25:11 INFO: All 0 tables opened in 0s +badger 2019/07/16 18:25:11 INFO: Replaying file id: 0 at offset: 0 +badger 2019/07/16 18:25:11 INFO: Replay took: 300.4s +I[2019-07-16|18:25:11.523] Starting ABCIServer impl=ABCIServ +``` + +Then we need to start Tendermint Core and point it to our application. Staying +within the application directory execute: + +```sh +$ TMHOME="/tmp/example" tendermint node --proxy_app=unix://example.sock + +I[2019-07-16|18:26:20.362] Version info module=main software=0.32.1 block=10 p2p=7 +I[2019-07-16|18:26:20.383] Starting Node module=main impl=Node +E[2019-07-16|18:26:20.392] Couldn't connect to any seeds module=p2p +I[2019-07-16|18:26:20.394] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:8dab80770ae8e295d4ce905d86af78c4ff634b79 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-nIO96P Version:0.32.1 Channels:4020212223303800 Moniker:app48.fun-box.ru Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}" +I[2019-07-16|18:26:21.440] Executed block module=state height=1 validTxs=0 invalidTxs=0 +I[2019-07-16|18:26:21.446] Committed state module=state height=1 txs=0 appHash= +``` + +This should start the full node and connect to our ABCI application. + +``` +I[2019-07-16|18:25:11.525] Waiting for new connection... +I[2019-07-16|18:26:20.329] Accepted a new connection +I[2019-07-16|18:26:20.329] Waiting for new connection... +I[2019-07-16|18:26:20.330] Accepted a new connection +I[2019-07-16|18:26:20.330] Waiting for new connection... +I[2019-07-16|18:26:20.330] Accepted a new connection +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB", + "height": "33" +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +``` +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3My" + } + } +} +``` + +"dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of +"tendermint" and "rocks" accordingly. From e1b4dddfae3e32f9c4c3c19d77cafe3b6224a48b Mon Sep 17 00:00:00 2001 From: Andy Nogueira <45477427+andynog@users.noreply.github.com> Date: Mon, 17 Jun 2019 06:51:12 -0400 Subject: [PATCH 123/211] docs: missing 'b' in python command (#3728) In Python 3 the command outlined in this doc `import codecs; codecs.decode("YWJjZA==", 'base64').decode('ascii')` throws an error: TypeError: decoding with 'base64' codec failed (TypeError: expected bytes-like object, not str), needs to add 'b' before the encoded string `import codecs; codecs.decode(b"YWJjZA==", 'base64').decode('ascii')` to make it work --- docs/app-dev/getting-started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/app-dev/getting-started.md b/docs/app-dev/getting-started.md index 9110761e2..eff70db68 100644 --- a/docs/app-dev/getting-started.md +++ b/docs/app-dev/getting-started.md @@ -137,7 +137,7 @@ The result should look like: Note the `value` in the result (`YWJjZA==`); this is the base64-encoding of the ASCII of `abcd`. You can verify this in a python 2 shell by running `"YWJjZA==".decode('base64')` or in python 3 shell by running -`import codecs; codecs.decode("YWJjZA==", 'base64').decode('ascii')`. +`import codecs; codecs.decode(b"YWJjZA==", 'base64').decode('ascii')`. Stay tuned for a future release that [makes this output more human-readable](https://github.com/tendermint/tendermint/issues/1794). From 244736a383db2afb82cd7565220eb48590201a7a Mon Sep 17 00:00:00 2001 From: Runchao Han Date: Mon, 17 Jun 2019 21:30:12 +1000 Subject: [PATCH 124/211] p2p: refactor Switch#OnStop (#3729) --- node/node.go | 1 - p2p/switch.go | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/node/node.go b/node/node.go index 46185847e..6f2cad1e5 100644 --- a/node/node.go +++ b/node/node.go @@ -714,7 +714,6 @@ func (n *Node) OnStop() { n.indexerService.Stop() // now stop the reactors - // TODO: gracefully disconnect from peers. n.sw.Stop() // stop mempool WAL diff --git a/p2p/switch.go b/p2p/switch.go index 31e0aa6e1..7e681d67c 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -221,11 +221,7 @@ func (sw *Switch) OnStart() error { func (sw *Switch) OnStop() { // Stop peers for _, p := range sw.peers.List() { - sw.transport.Cleanup(p) - p.Stop() - if sw.peers.Remove(p) { - sw.metrics.Peers.Add(float64(-1)) - } + sw.stopAndRemovePeer(p, nil) } // Stop reactors From 393d0aa3c5e55848491aa6bd5ca883211bbc7b97 Mon Sep 17 00:00:00 2001 From: Hans Schoenburg Date: Wed, 19 Jun 2019 20:35:53 +0200 Subject: [PATCH 125/211] docs: fix some language issues and deprecated link (#3733) --- docs/.vuepress/config.js | 2 +- docs/app-dev/app-development.md | 6 +++--- docs/spec/abci/abci.md | 2 +- docs/spec/blockchain/encoding.md | 2 +- docs/spec/blockchain/state.md | 2 +- docs/tendermint-core/using-tendermint.md | 4 +++- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 0b54d2011..9adfc5953 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -44,7 +44,7 @@ module.exports = { "/app-dev/app-development", "/app-dev/subscribing-to-events-via-websocket", "/app-dev/indexing-transactions", - "/app-dev/abci-spec", + "/spec/abci/abci", "/app-dev/ecosystem" ] }, diff --git a/docs/app-dev/app-development.md b/docs/app-dev/app-development.md index d157ce378..1b4e26800 100644 --- a/docs/app-dev/app-development.md +++ b/docs/app-dev/app-development.md @@ -133,8 +133,8 @@ the mempool. If Tendermint is just started or the clients sent more than 100k transactions, old transactions may be sent to the application. So it is important CheckTx implements some logic to handle them. -There are cases where a transaction will (or may) become valid in some -future state, in which case you probably want to disable Tendermint's +If there are cases in your application where a transaction may become invalid in some +future state, you probably want to disable Tendermint's cache. You can do that by setting `[mempool] cache_size = 0` in the config. @@ -205,7 +205,7 @@ Once all processing of the block is complete, Tendermint sends the Commit request and blocks waiting for a response. While the mempool may run concurrently with block processing (the BeginBlock, DeliverTxs, and EndBlock), it is locked for the Commit request so that its state can be -safely reset during Commit. This means the app _MUST NOT_ do any +safely updated during Commit. This means the app _MUST NOT_ do any blocking communication with the mempool (ie. broadcast_tx) during Commit, or there will be deadlock. Note also that all remaining transactions in the mempool are replayed on the mempool connection diff --git a/docs/spec/abci/abci.md b/docs/spec/abci/abci.md index c65d96ec1..aa58dec55 100644 --- a/docs/spec/abci/abci.md +++ b/docs/spec/abci/abci.md @@ -103,7 +103,7 @@ on them. All other fields in the `Response*` must be strictly deterministic. ## Block Execution The first time a new blockchain is started, Tendermint calls -`InitChain`. From then on, the follow sequence of methods is executed for each +`InitChain`. From then on, the following sequence of methods is executed for each block: `BeginBlock, [DeliverTx], EndBlock, Commit` diff --git a/docs/spec/blockchain/encoding.md b/docs/spec/blockchain/encoding.md index bde580a14..14d0e786b 100644 --- a/docs/spec/blockchain/encoding.md +++ b/docs/spec/blockchain/encoding.md @@ -218,7 +218,7 @@ func MerkleRoot(items [][]byte) []byte{ case 0: return nil case 1: - return leafHash(leafs[0]) + return leafHash(items[0]) default: k := getSplitPoint(len(items)) left := MerkleRoot(items[:k]) diff --git a/docs/spec/blockchain/state.md b/docs/spec/blockchain/state.md index 3ab65e12b..15fc37761 100644 --- a/docs/spec/blockchain/state.md +++ b/docs/spec/blockchain/state.md @@ -59,7 +59,7 @@ type Validator struct { When hashing the Validator struct, the address is not included, because it is redundant with the pubkey. -The `state.Validators`, `state.LastValidators`, and `state.NextValidators`, must always by sorted by validator address, +The `state.Validators`, `state.LastValidators`, and `state.NextValidators`, must always be sorted by validator address, so that there is a canonical order for computing the MerkleRoot. We also define a `TotalVotingPower` function, to return the total voting power: diff --git a/docs/tendermint-core/using-tendermint.md b/docs/tendermint-core/using-tendermint.md index 2ca8c9e92..05d481b2c 100644 --- a/docs/tendermint-core/using-tendermint.md +++ b/docs/tendermint-core/using-tendermint.md @@ -202,8 +202,10 @@ Note that raw hex cannot be used in `POST` transactions. ## Reset -**WARNING: UNSAFE** Only do this in development and only if you can +::: warning +**UNSAFE** Only do this in development and only if you can afford to lose all blockchain data! +::: To reset a blockchain, stop the node and run: From 7f64d875aaf867fdff19bad1a31ed95150f87451 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 21 Jun 2019 09:30:32 +0400 Subject: [PATCH 126/211] node: fix a bug where `nil` is recorded as node's address (#3740) * node: fix a bug where `nil` is recorded as node's address Solution AddOurAddress when we know it sw.NetAddress is nil in createAddrBookAndSetOnSwitch it's set by n.transport.Listen function, which is called during start Fixes #3716 * use addr instead of n.sw.NetAddress * add both ExternalAddress and ListenAddress as our addresses --- CHANGELOG_PENDING.md | 5 +++-- node/node.go | 24 ++++++++++++++++++++---- 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 45ed56919..4bbe9ebbd 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -33,5 +33,6 @@ * [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. ### BUG FIXES: -- [libs/db] Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) -- [libs/db] Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) +- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [node] \#3716 Fix a bug where `nil` is recorded as node's address diff --git a/node/node.go b/node/node.go index 6f2cad1e5..85fef5ee7 100644 --- a/node/node.go +++ b/node/node.go @@ -441,17 +441,30 @@ func createSwitch(config *cfg.Config, } func createAddrBookAndSetOnSwitch(config *cfg.Config, sw *p2p.Switch, - p2pLogger log.Logger) pex.AddrBook { + p2pLogger log.Logger, nodeKey *p2p.NodeKey) (pex.AddrBook, error) { addrBook := pex.NewAddrBook(config.P2P.AddrBookFile(), config.P2P.AddrBookStrict) addrBook.SetLogger(p2pLogger.With("book", config.P2P.AddrBookFile())) // Add ourselves to addrbook to prevent dialing ourselves - addrBook.AddOurAddress(sw.NetAddress()) + if config.P2P.ExternalAddress != "" { + addr, err := p2p.NewNetAddressString(p2p.IDAddressString(nodeKey.ID(), config.P2P.ExternalAddress)) + if err != nil { + return nil, errors.Wrap(err, "p2p.external_address is incorrect") + } + addrBook.AddOurAddress(addr) + } + if config.P2P.ListenAddress != "" { + addr, err := p2p.NewNetAddressString(p2p.IDAddressString(nodeKey.ID(), config.P2P.ListenAddress)) + if err != nil { + return nil, errors.Wrap(err, "p2p.laddr is incorrect") + } + addrBook.AddOurAddress(addr) + } sw.SetAddrBook(addrBook) - return addrBook + return addrBook, nil } func createPEXReactorAndAddToSwitch(addrBook pex.AddrBook, config *cfg.Config, @@ -594,7 +607,10 @@ func NewNode(config *cfg.Config, return nil, errors.Wrap(err, "could not add peers from persistent_peers field") } - addrBook := createAddrBookAndSetOnSwitch(config, sw, p2pLogger) + addrBook, err := createAddrBookAndSetOnSwitch(config, sw, p2pLogger, nodeKey) + if err != nil { + return nil, errors.Wrap(err, "could not create addrbook") + } // Optionally, start the pex reactor // From d37185ffe790f02817e4c817ff716c0f63d260bf Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 21 Jun 2019 01:56:27 -0400 Subject: [PATCH 127/211] abci: Refactor ABCI CheckTx and DeliverTx signatures (#3735) * Refactor signature of Application.CheckTx * Refactor signature of Application.DeliverTx * Refactor example variable names for clarity and consistency * Rename method variables for consistency * Rename method variables for consistency * add a changelog entry * update docs --- CHANGELOG_PENDING.md | 1 + abci/client/local_client.go | 8 +- abci/example/counter/counter.go | 16 +-- abci/example/kvstore/kvstore.go | 8 +- abci/example/kvstore/kvstore_test.go | 7 +- abci/example/kvstore/persistent_kvstore.go | 12 +- abci/server/socket_server.go | 4 +- abci/types/application.go | 12 +- blockchain/reactor_test.go | 2 +- consensus/mempool_test.go | 10 +- consensus/replay.go | 2 +- docs/app-dev/app-development.md | 139 +++++++++++---------- docs/app-dev/indexing-transactions.md | 2 +- mempool/clist_mempool_test.go | 2 +- rpc/client/mock/abci.go | 12 +- state/execution_test.go | 2 +- 16 files changed, 125 insertions(+), 114 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 4bbe9ebbd..76c01bbbc 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,6 +20,7 @@ * If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID +- [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI interface * Blockchain Protocol diff --git a/abci/client/local_client.go b/abci/client/local_client.go index d0e50c330..4a3e6a5ee 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -85,7 +85,7 @@ func (app *localClient) DeliverTxAsync(tx []byte) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.DeliverTx(tx) + res := app.Application.DeliverTx(types.RequestDeliverTx{Tx: tx}) return app.callback( types.ToRequestDeliverTx(tx), types.ToResponseDeliverTx(res), @@ -96,7 +96,7 @@ func (app *localClient) CheckTxAsync(tx []byte) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.CheckTx(tx) + res := app.Application.CheckTx(types.RequestCheckTx{Tx: tx}) return app.callback( types.ToRequestCheckTx(tx), types.ToResponseCheckTx(res), @@ -188,7 +188,7 @@ func (app *localClient) DeliverTxSync(tx []byte) (*types.ResponseDeliverTx, erro app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.DeliverTx(tx) + res := app.Application.DeliverTx(types.RequestDeliverTx{Tx: tx}) return &res, nil } @@ -196,7 +196,7 @@ func (app *localClient) CheckTxSync(tx []byte) (*types.ResponseCheckTx, error) { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.CheckTx(tx) + res := app.Application.CheckTx(types.RequestCheckTx{Tx: tx}) return &res, nil } diff --git a/abci/example/counter/counter.go b/abci/example/counter/counter.go index a77e7821f..2cea1e558 100644 --- a/abci/example/counter/counter.go +++ b/abci/example/counter/counter.go @@ -42,15 +42,15 @@ func (app *CounterApplication) SetOption(req types.RequestSetOption) types.Respo return types.ResponseSetOption{} } -func (app *CounterApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { +func (app *CounterApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { if app.serial { - if len(tx) > 8 { + if len(req.Tx) > 8 { return types.ResponseDeliverTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(tx))} + Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(req.Tx))} } tx8 := make([]byte, 8) - copy(tx8[len(tx8)-len(tx):], tx) + copy(tx8[len(tx8)-len(req.Tx):], req.Tx) txValue := binary.BigEndian.Uint64(tx8) if txValue != uint64(app.txCount) { return types.ResponseDeliverTx{ @@ -62,15 +62,15 @@ func (app *CounterApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { return types.ResponseDeliverTx{Code: code.CodeTypeOK} } -func (app *CounterApplication) CheckTx(tx []byte) types.ResponseCheckTx { +func (app *CounterApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { if app.serial { - if len(tx) > 8 { + if len(req.Tx) > 8 { return types.ResponseCheckTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(tx))} + Log: fmt.Sprintf("Max tx size is 8 bytes, got %d", len(req.Tx))} } tx8 := make([]byte, 8) - copy(tx8[len(tx8)-len(tx):], tx) + copy(tx8[len(tx8)-len(req.Tx):], req.Tx) txValue := binary.BigEndian.Uint64(tx8) if txValue < uint64(app.txCount) { return types.ResponseCheckTx{ diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index 955baefb4..27fdfda86 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -76,13 +76,13 @@ func (app *KVStoreApplication) Info(req types.RequestInfo) (resInfo types.Respon } // tx is either "key=value" or just arbitrary bytes -func (app *KVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { +func (app *KVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { var key, value []byte - parts := bytes.Split(tx, []byte("=")) + parts := bytes.Split(req.Tx, []byte("=")) if len(parts) == 2 { key, value = parts[0], parts[1] } else { - key, value = tx, tx + key, value = req.Tx, req.Tx } app.state.db.Set(prefixKey(key), value) app.state.Size += 1 @@ -94,7 +94,7 @@ func (app *KVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { return types.ResponseDeliverTx{Code: code.CodeTypeOK, Tags: tags} } -func (app *KVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx { +func (app *KVStoreApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { return types.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1} } diff --git a/abci/example/kvstore/kvstore_test.go b/abci/example/kvstore/kvstore_test.go index a18fb8d3c..074baa49f 100644 --- a/abci/example/kvstore/kvstore_test.go +++ b/abci/example/kvstore/kvstore_test.go @@ -19,10 +19,11 @@ import ( ) func testKVStore(t *testing.T, app types.Application, tx []byte, key, value string) { - ar := app.DeliverTx(tx) + req := types.RequestDeliverTx{Tx: tx} + ar := app.DeliverTx(req) require.False(t, ar.IsErr(), ar) // repeating tx doesn't raise error - ar = app.DeliverTx(tx) + ar = app.DeliverTx(req) require.False(t, ar.IsErr(), ar) // make sure query is fine @@ -179,7 +180,7 @@ func makeApplyBlock(t *testing.T, kvstore types.Application, heightInt int, diff kvstore.BeginBlock(types.RequestBeginBlock{Hash: hash, Header: header}) for _, tx := range txs { - if r := kvstore.DeliverTx(tx); r.IsErr() { + if r := kvstore.DeliverTx(types.RequestDeliverTx{Tx: tx}); r.IsErr() { t.Fatal(r) } } diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index f969eebfe..b7484a4aa 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -61,21 +61,21 @@ func (app *PersistentKVStoreApplication) SetOption(req types.RequestSetOption) t } // tx is either "val:pubkey/power" or "key=value" or just arbitrary bytes -func (app *PersistentKVStoreApplication) DeliverTx(tx []byte) types.ResponseDeliverTx { +func (app *PersistentKVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { // if it starts with "val:", update the validator set // format is "val:pubkey/power" - if isValidatorTx(tx) { + if isValidatorTx(req.Tx) { // update validators in the merkle tree // and in app.ValUpdates - return app.execValidatorTx(tx) + return app.execValidatorTx(req.Tx) } // otherwise, update the key-value store - return app.app.DeliverTx(tx) + return app.app.DeliverTx(req) } -func (app *PersistentKVStoreApplication) CheckTx(tx []byte) types.ResponseCheckTx { - return app.app.CheckTx(tx) +func (app *PersistentKVStoreApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { + return app.app.CheckTx(req) } // Commit will panic if InitChain was not called diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 4b92f04cf..96cb844b7 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -178,10 +178,10 @@ func (s *SocketServer) handleRequest(req *types.Request, responses chan<- *types res := s.app.SetOption(*r.SetOption) responses <- types.ToResponseSetOption(res) case *types.Request_DeliverTx: - res := s.app.DeliverTx(r.DeliverTx.Tx) + res := s.app.DeliverTx(*r.DeliverTx) responses <- types.ToResponseDeliverTx(res) case *types.Request_CheckTx: - res := s.app.CheckTx(r.CheckTx.Tx) + res := s.app.CheckTx(*r.CheckTx) responses <- types.ToResponseCheckTx(res) case *types.Request_Commit: res := s.app.Commit() diff --git a/abci/types/application.go b/abci/types/application.go index 88f8d001e..90518851d 100644 --- a/abci/types/application.go +++ b/abci/types/application.go @@ -15,12 +15,12 @@ type Application interface { Query(RequestQuery) ResponseQuery // Query for state // Mempool Connection - CheckTx(tx []byte) ResponseCheckTx // Validate a tx for the mempool + CheckTx(RequestCheckTx) ResponseCheckTx // Validate a tx for the mempool // Consensus Connection InitChain(RequestInitChain) ResponseInitChain // Initialize blockchain with validators and other info from TendermintCore BeginBlock(RequestBeginBlock) ResponseBeginBlock // Signals the beginning of a block - DeliverTx(tx []byte) ResponseDeliverTx // Deliver a tx for full processing + DeliverTx(RequestDeliverTx) ResponseDeliverTx // Deliver a tx for full processing EndBlock(RequestEndBlock) ResponseEndBlock // Signals the end of a block, returns changes to the validator set Commit() ResponseCommit // Commit the state and return the application Merkle root hash } @@ -45,11 +45,11 @@ func (BaseApplication) SetOption(req RequestSetOption) ResponseSetOption { return ResponseSetOption{} } -func (BaseApplication) DeliverTx(tx []byte) ResponseDeliverTx { +func (BaseApplication) DeliverTx(req RequestDeliverTx) ResponseDeliverTx { return ResponseDeliverTx{Code: CodeTypeOK} } -func (BaseApplication) CheckTx(tx []byte) ResponseCheckTx { +func (BaseApplication) CheckTx(req RequestCheckTx) ResponseCheckTx { return ResponseCheckTx{Code: CodeTypeOK} } @@ -103,12 +103,12 @@ func (app *GRPCApplication) SetOption(ctx context.Context, req *RequestSetOption } func (app *GRPCApplication) DeliverTx(ctx context.Context, req *RequestDeliverTx) (*ResponseDeliverTx, error) { - res := app.app.DeliverTx(req.Tx) + res := app.app.DeliverTx(*req) return &res, nil } func (app *GRPCApplication) CheckTx(ctx context.Context, req *RequestCheckTx) (*ResponseCheckTx, error) { - res := app.app.CheckTx(req.Tx) + res := app.app.CheckTx(*req) return &res, nil } diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 52ef9ba64..8d476331a 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -295,7 +295,7 @@ func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} } -func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { +func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { return abci.ResponseCheckTx{} } diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index af15a1fef..d9feef9b4 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -141,7 +141,7 @@ func TestMempoolRmBadTx(t *testing.T) { txBytes := make([]byte, 8) binary.BigEndian.PutUint64(txBytes, uint64(0)) - resDeliver := app.DeliverTx(txBytes) + resDeliver := app.DeliverTx(abci.RequestDeliverTx{Tx: txBytes}) assert.False(t, resDeliver.IsErr(), fmt.Sprintf("expected no error. got %v", resDeliver)) resCommit := app.Commit() @@ -209,8 +209,8 @@ func (app *CounterApplication) Info(req abci.RequestInfo) abci.ResponseInfo { return abci.ResponseInfo{Data: fmt.Sprintf("txs:%v", app.txCount)} } -func (app *CounterApplication) DeliverTx(tx []byte) abci.ResponseDeliverTx { - txValue := txAsUint64(tx) +func (app *CounterApplication) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { + txValue := txAsUint64(req.Tx) if txValue != uint64(app.txCount) { return abci.ResponseDeliverTx{ Code: code.CodeTypeBadNonce, @@ -220,8 +220,8 @@ func (app *CounterApplication) DeliverTx(tx []byte) abci.ResponseDeliverTx { return abci.ResponseDeliverTx{Code: code.CodeTypeOK} } -func (app *CounterApplication) CheckTx(tx []byte) abci.ResponseCheckTx { - txValue := txAsUint64(tx) +func (app *CounterApplication) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { + txValue := txAsUint64(req.Tx) if txValue != uint64(app.mempoolTxCount) { return abci.ResponseCheckTx{ Code: code.CodeTypeBadNonce, diff --git a/consensus/replay.go b/consensus/replay.go index 794f870d0..2c4377ffa 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -515,7 +515,7 @@ type mockProxyApp struct { abciResponses *sm.ABCIResponses } -func (mock *mockProxyApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { +func (mock *mockProxyApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { r := mock.abciResponses.DeliverTx[mock.txCount] mock.txCount++ if r == nil { //it could be nil because of amino unMarshall, it will cause an empty ResponseDeliverTx to become nil diff --git a/docs/app-dev/app-development.md b/docs/app-dev/app-development.md index 1b4e26800..c9983beaa 100644 --- a/docs/app-dev/app-development.md +++ b/docs/app-dev/app-development.md @@ -101,8 +101,8 @@ mempool state (this behaviour can be turned off with In go: ``` -func (app *KVStoreApplication) CheckTx(tx []byte) types.Result { - return types.OK +func (app *KVStoreApplication) CheckTx(req types.RequestCheckTx) types.ResponseCheckTx { + return types.ResponseCheckTx{Code: code.CodeTypeOK, GasWanted: 1} } ``` @@ -168,14 +168,29 @@ In go: ``` // tx is either "key=value" or just arbitrary bytes -func (app *KVStoreApplication) DeliverTx(tx []byte) types.Result { - parts := strings.Split(string(tx), "=") - if len(parts) == 2 { - app.state.Set([]byte(parts[0]), []byte(parts[1])) - } else { - app.state.Set(tx, tx) - } - return types.OK +func (app *KVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { + var key, value []byte + parts := bytes.Split(req.Tx, []byte("=")) + if len(parts) == 2 { + key, value = parts[0], parts[1] + } else { + key, value = req.Tx, req.Tx + } + + app.state.db.Set(prefixKey(key), value) + app.state.Size += 1 + + events := []types.Event{ + { + Type: "app", + Attributes: []cmn.KVPair{ + {Key: []byte("creator"), Value: []byte("Cosmoshi Netowoko")}, + {Key: []byte("key"), Value: key}, + }, + }, + } + + return types.ResponseDeliverTx{Code: code.CodeTypeOK, Events: events} } ``` @@ -223,9 +238,14 @@ job of the [Handshake](#handshake). In go: ``` -func (app *KVStoreApplication) Commit() types.Result { - hash := app.state.Hash() - return types.NewResultOK(hash, "") +func (app *KVStoreApplication) Commit() types.ResponseCommit { + // Using a memdb - just return the big endian size of the db + appHash := make([]byte, 8) + binary.PutVarint(appHash, app.state.Size) + app.state.AppHash = appHash + app.state.Height += 1 + saveState(app.state) + return types.ResponseCommit{Data: appHash} } ``` @@ -256,12 +276,10 @@ In go: ``` // Track the block hash and header information -func (app *PersistentKVStoreApplication) BeginBlock(params types.RequestBeginBlock) { - // update latest block info - app.blockHeader = params.Header - - // reset valset changes - app.changes = make([]*types.Validator, 0) +func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock { + // reset valset changes + app.ValUpdates = make([]types.ValidatorUpdate, 0) + return types.ResponseBeginBlock{} } ``` @@ -303,7 +321,7 @@ In go: ``` // Update the validator set func (app *PersistentKVStoreApplication) EndBlock(req types.RequestEndBlock) types.ResponseEndBlock { - return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} + return types.ResponseEndBlock{ValidatorUpdates: app.ValUpdates} } ``` @@ -347,43 +365,29 @@ Note: these query formats are subject to change! In go: ``` - func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { - if reqQuery.Prove { - value, proof, exists := app.state.GetWithProof(reqQuery.Data) - resQuery.Index = -1 // TODO make Proof return index - resQuery.Key = reqQuery.Data - resQuery.Value = value - resQuery.Proof = proof - if exists { - resQuery.Log = "exists" - } else { - resQuery.Log = "does not exist" - } - return - } else { - index, value, exists := app.state.Get(reqQuery.Data) - resQuery.Index = int64(index) - resQuery.Value = value - if exists { - resQuery.Log = "exists" - } else { - resQuery.Log = "does not exist" - } - return - } - } - return - } else { - index, value, exists := app.state.Get(reqQuery.Data) - resQuery.Index = int64(index) - resQuery.Value = value - if exists { - resQuery.Log = "exists" - } else { - resQuery.Log = "does not exist" - } - return - } +func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { + if reqQuery.Prove { + value := app.state.db.Get(prefixKey(reqQuery.Data)) + resQuery.Index = -1 // TODO make Proof return index + resQuery.Key = reqQuery.Data + resQuery.Value = value + if value != nil { + resQuery.Log = "exists" + } else { + resQuery.Log = "does not exist" + } + return + } else { + resQuery.Key = reqQuery.Data + value := app.state.db.Get(prefixKey(reqQuery.Data)) + resQuery.Value = value + if value != nil { + resQuery.Log = "exists" + } else { + resQuery.Log = "does not exist" + } + return + } } ``` @@ -439,7 +443,11 @@ In go: ``` func (app *KVStoreApplication) Info(req types.RequestInfo) (resInfo types.ResponseInfo) { - return types.ResponseInfo{Data: fmt.Sprintf("{\"size\":%v}", app.state.Size())} + return types.ResponseInfo{ + Data: fmt.Sprintf("{\"size\":%v}", app.state.Size), + Version: version.ABCIVersion, + AppVersion: ProtocolVersion.Uint64(), + } } ``` @@ -463,13 +471,14 @@ In go: ``` // Save the validators in the merkle tree -func (app *PersistentKVStoreApplication) InitChain(params types.RequestInitChain) { - for _, v := range params.Validators { - r := app.updateValidator(v) - if r.IsErr() { - app.logger.Error("Error updating validators", "r", r) - } - } +func (app *PersistentKVStoreApplication) InitChain(req types.RequestInitChain) types.ResponseInitChain { + for _, v := range req.Validators { + r := app.updateValidator(v) + if r.IsErr() { + app.logger.Error("Error updating validators", "r", r) + } + } + return types.ResponseInitChain{} } ``` diff --git a/docs/app-dev/indexing-transactions.md b/docs/app-dev/indexing-transactions.md index de8336a43..ffe8b989a 100644 --- a/docs/app-dev/indexing-transactions.md +++ b/docs/app-dev/indexing-transactions.md @@ -47,7 +47,7 @@ pairs of UTF-8 encoded strings (e.g. "account.owner": "Bob", "balance": Example: ``` -func (app *KVStoreApplication) DeliverTx(tx []byte) types.Result { +func (app *KVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.Result { ... tags := []cmn.KVPair{ {[]byte("account.name"), []byte("igor")}, diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index bf2c61dd7..db6e800b8 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -99,7 +99,7 @@ func TestReapMaxBytesMaxGas(t *testing.T) { checkTxs(t, mempool, 1, UnknownPeerID) tx0 := mempool.TxsFront().Value.(*mempoolTx) // assert that kv store has gas wanted = 1. - require.Equal(t, app.CheckTx(tx0.tx).GasWanted, int64(1), "KVStore had a gas value neq to 1") + require.Equal(t, app.CheckTx(abci.RequestCheckTx{Tx: tx0.tx}).GasWanted, int64(1), "KVStore had a gas value neq to 1") require.Equal(t, tx0.gasWanted, int64(1), "transactions gas was set incorrectly") // ensure each tx is 20 bytes long require.Equal(t, len(tx0.tx), 20, "Tx is longer than 20 bytes") diff --git a/rpc/client/mock/abci.go b/rpc/client/mock/abci.go index 2ab62a420..f40755fec 100644 --- a/rpc/client/mock/abci.go +++ b/rpc/client/mock/abci.go @@ -45,29 +45,29 @@ func (a ABCIApp) ABCIQueryWithOptions(path string, data cmn.HexBytes, opts clien // TODO: Make it wait for a commit and set res.Height appropriately. func (a ABCIApp) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { res := ctypes.ResultBroadcastTxCommit{} - res.CheckTx = a.App.CheckTx(tx) + res.CheckTx = a.App.CheckTx(abci.RequestCheckTx{Tx: tx}) if res.CheckTx.IsErr() { return &res, nil } - res.DeliverTx = a.App.DeliverTx(tx) + res.DeliverTx = a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) res.Height = -1 // TODO return &res, nil } func (a ABCIApp) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - c := a.App.CheckTx(tx) + c := a.App.CheckTx(abci.RequestCheckTx{Tx: tx}) // and this gets written in a background thread... if !c.IsErr() { - go func() { a.App.DeliverTx(tx) }() // nolint: errcheck + go func() { a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) }() // nolint: errcheck } return &ctypes.ResultBroadcastTx{Code: c.Code, Data: c.Data, Log: c.Log, Hash: tx.Hash()}, nil } func (a ABCIApp) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - c := a.App.CheckTx(tx) + c := a.App.CheckTx(abci.RequestCheckTx{Tx: tx}) // and this gets written in a background thread... if !c.IsErr() { - go func() { a.App.DeliverTx(tx) }() // nolint: errcheck + go func() { a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) }() // nolint: errcheck } return &ctypes.ResultBroadcastTx{Code: c.Code, Data: c.Data, Log: c.Log, Hash: tx.Hash()}, nil } diff --git a/state/execution_test.go b/state/execution_test.go index 465b6b0be..6a23929b6 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -458,7 +458,7 @@ func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} } -func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { +func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { return abci.ResponseCheckTx{} } From 53c0074f8bea72576542f557b9bb5a9e20e0b029 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 21 Jun 2019 07:58:32 +0200 Subject: [PATCH 128/211] Changes to files that had linting issue (#3731) - Govet issues fixed - 1 gosec issue solved using nolint Signed-off-by: Marko Baricevic --- .golangci.yml | 1 - benchmarks/codec_test.go | 2 +- blockchain/reactor_test.go | 2 +- consensus/byzantine_test.go | 4 ++-- consensus/common_test.go | 4 ++-- consensus/reactor.go | 4 ---- consensus/state_test.go | 4 ++-- consensus/types/height_vote_set_test.go | 2 +- evidence/pool_test.go | 2 +- go.sum | 1 + node/node_test.go | 8 +++---- p2p/upnp/upnp.go | 2 +- privval/file_test.go | 14 ++++++------ state/execution_test.go | 20 ++++++++--------- state/state_test.go | 30 ++++++++++++------------- state/tx_filter_test.go | 2 +- state/txindex/indexer_service_test.go | 4 ++-- types/block.go | 2 +- types/protobuf_test.go | 2 +- 19 files changed, 53 insertions(+), 57 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index a051e1a45..6adbbd9da 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -20,7 +20,6 @@ linters: - gochecknoinits - scopelint - stylecheck - # linters-settings: # govet: # check-shadowing: true diff --git a/benchmarks/codec_test.go b/benchmarks/codec_test.go index eff5c7349..64c0e72cf 100644 --- a/benchmarks/codec_test.go +++ b/benchmarks/codec_test.go @@ -14,7 +14,7 @@ import ( func testNodeInfo(id p2p.ID) p2p.DefaultNodeInfo { return p2p.DefaultNodeInfo{ - ProtocolVersion: p2p.ProtocolVersion{1, 2, 3}, + ProtocolVersion: p2p.ProtocolVersion{P2P: 1, Block: 2, App: 3}, ID_: id, Moniker: "SOMENAME", Network: "SOMENAME", diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 8d476331a..d5dcab1a7 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -111,7 +111,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals thisBlock := makeBlock(blockHeight, state, lastCommit) thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) - blockID := types.BlockID{thisBlock.Hash(), thisParts.Header()} + blockID := types.BlockID{Hash: thisBlock.Hash(), PartsHeader: thisParts.Header()} state, err = blockExec.ApplyBlock(state, blockID, thisBlock) if err != nil { diff --git a/consensus/byzantine_test.go b/consensus/byzantine_test.go index c2eb114dc..1c52e79ad 100644 --- a/consensus/byzantine_test.go +++ b/consensus/byzantine_test.go @@ -177,7 +177,7 @@ func byzantineDecideProposalFunc(t *testing.T, height int64, round int, cs *Cons // Create a new proposal block from state/txs from the mempool. block1, blockParts1 := cs.createProposalBlock() - polRound, propBlockID := cs.ValidRound, types.BlockID{block1.Hash(), blockParts1.Header()} + polRound, propBlockID := cs.ValidRound, types.BlockID{Hash: block1.Hash(), PartsHeader: blockParts1.Header()} proposal1 := types.NewProposal(height, round, polRound, propBlockID) if err := cs.privValidator.SignProposal(cs.state.ChainID, proposal1); err != nil { t.Error(err) @@ -185,7 +185,7 @@ func byzantineDecideProposalFunc(t *testing.T, height int64, round int, cs *Cons // Create a new proposal block from state/txs from the mempool. block2, blockParts2 := cs.createProposalBlock() - polRound, propBlockID = cs.ValidRound, types.BlockID{block2.Hash(), blockParts2.Header()} + polRound, propBlockID = cs.ValidRound, types.BlockID{Hash: block2.Hash(), PartsHeader: blockParts2.Header()} proposal2 := types.NewProposal(height, round, polRound, propBlockID) if err := cs.privValidator.SignProposal(cs.state.ChainID, proposal2); err != nil { t.Error(err) diff --git a/consensus/common_test.go b/consensus/common_test.go index a4ad79c94..29db524ec 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -86,7 +86,7 @@ func (vs *validatorStub) signVote(voteType types.SignedMsgType, hash []byte, hea Round: vs.Round, Timestamp: tmtime.Now(), Type: voteType, - BlockID: types.BlockID{hash, header}, + BlockID: types.BlockID{Hash: hash, PartsHeader: header}, } err := vs.PrivValidator.SignVote(config.ChainID(), vote) return vote, err @@ -159,7 +159,7 @@ func decideProposal(cs1 *ConsensusState, vs *validatorStub, height int64, round } // Make proposal - polRound, propBlockID := validRound, types.BlockID{block.Hash(), blockParts.Header()} + polRound, propBlockID := validRound, types.BlockID{Hash: block.Hash(), PartsHeader: blockParts.Header()} proposal = types.NewProposal(height, round, polRound, propBlockID) if err := vs.SignProposal(chainID, proposal); err != nil { panic(err) diff --git a/consensus/reactor.go b/consensus/reactor.go index 36e948f6d..f690a407d 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -351,10 +351,6 @@ func (conR *ConsensusReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) default: conR.Logger.Error(fmt.Sprintf("Unknown chId %X", chID)) } - - if err != nil { - conR.Logger.Error("Error in Receive()", "err", err) - } } // SetEventBus sets event bus. diff --git a/consensus/state_test.go b/consensus/state_test.go index 87e351dc8..93ef0d4cb 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -192,7 +192,7 @@ func TestStateBadProposal(t *testing.T) { stateHash[0] = byte((stateHash[0] + 1) % 255) propBlock.AppHash = stateHash propBlockParts := propBlock.MakePartSet(partSize) - blockID := types.BlockID{propBlock.Hash(), propBlockParts.Header()} + blockID := types.BlockID{Hash: propBlock.Hash(), PartsHeader: propBlockParts.Header()} proposal := types.NewProposal(vs2.Height, round, -1, blockID) if err := vs2.SignProposal(config.ChainID(), proposal); err != nil { t.Fatal("failed to sign bad proposal", err) @@ -811,7 +811,7 @@ func TestStateLockPOLSafety2(t *testing.T) { _, propBlock0 := decideProposal(cs1, vss[0], height, round) propBlockHash0 := propBlock0.Hash() propBlockParts0 := propBlock0.MakePartSet(partSize) - propBlockID0 := types.BlockID{propBlockHash0, propBlockParts0.Header()} + propBlockID0 := types.BlockID{Hash: propBlockHash0, PartsHeader: propBlockParts0.Header()} // the others sign a polka but we don't see it prevotes := signVotes(types.PrevoteType, propBlockHash0, propBlockParts0.Header(), vs2, vs3, vs4) diff --git a/consensus/types/height_vote_set_test.go b/consensus/types/height_vote_set_test.go index 42b5333a1..f45492aa4 100644 --- a/consensus/types/height_vote_set_test.go +++ b/consensus/types/height_vote_set_test.go @@ -62,7 +62,7 @@ func makeVoteHR(t *testing.T, height int64, round int, privVals []types.PrivVali Round: round, Timestamp: tmtime.Now(), Type: types.PrecommitType, - BlockID: types.BlockID{[]byte("fakehash"), types.PartSetHeader{}}, + BlockID: types.BlockID{Hash: []byte("fakehash"), PartsHeader: types.PartSetHeader{}}, } chainID := config.ChainID() err := privVal.SignVote(chainID, vote) diff --git a/evidence/pool_test.go b/evidence/pool_test.go index 30b20011e..13bc45563 100644 --- a/evidence/pool_test.go +++ b/evidence/pool_test.go @@ -60,7 +60,7 @@ func TestEvidencePool(t *testing.T) { pool := NewEvidencePool(stateDB, evidenceDB) goodEvidence := types.NewMockGoodEvidence(height, 0, valAddr) - badEvidence := types.MockBadEvidence{goodEvidence} + badEvidence := types.MockBadEvidence{MockGoodEvidence: goodEvidence} // bad evidence err := pool.AddEvidence(badEvidence) diff --git a/go.sum b/go.sum index 10e54c0fd..fee691de9 100644 --- a/go.sum +++ b/go.sum @@ -17,6 +17,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/etcd-io/bbolt v1.3.2 h1:RLRQ0TKLX7DlBRXAJHvbmXL17Q3KNnTBtZ9B6Qo+/Y0= github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= diff --git a/node/node_test.go b/node/node_test.go index 6971ddd34..ce4e82c2d 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -285,10 +285,10 @@ func state(nVals int, height int64) (sm.State, dbm.DB) { secret := []byte(fmt.Sprintf("test%d", i)) pk := ed25519.GenPrivKeyFromSecret(secret) vals[i] = types.GenesisValidator{ - pk.PubKey().Address(), - pk.PubKey(), - 1000, - fmt.Sprintf("test%d", i), + Address: pk.PubKey().Address(), + PubKey: pk.PubKey(), + Power: 1000, + Name: fmt.Sprintf("test%d", i), } } s, _ := sm.MakeGenesisState(&types.GenesisDoc{ diff --git a/p2p/upnp/upnp.go b/p2p/upnp/upnp.go index d53974fc4..89f35c5df 100644 --- a/p2p/upnp/upnp.go +++ b/p2p/upnp/upnp.go @@ -197,7 +197,7 @@ func localIPv4() (net.IP, error) { } func getServiceURL(rootURL string) (url, urnDomain string, err error) { - r, err := http.Get(rootURL) + r, err := http.Get(rootURL) // nolint: gosec if err != nil { return } diff --git a/privval/file_test.go b/privval/file_test.go index 06d75a809..98de69480 100644 --- a/privval/file_test.go +++ b/privval/file_test.go @@ -50,7 +50,7 @@ func TestResetValidator(t *testing.T) { // test vote height, round := int64(10), 1 voteType := byte(types.PrevoteType) - blockID := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} + blockID := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} vote := newVote(privVal.Key.Address, 0, height, round, voteType, blockID) err = privVal.SignVote("mychainid", vote) assert.NoError(t, err, "expected no error signing vote") @@ -162,8 +162,8 @@ func TestSignVote(t *testing.T) { privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) - block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} - block2 := types.BlockID{[]byte{3, 2, 1}, types.PartSetHeader{}} + block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} + block2 := types.BlockID{Hash: []byte{3, 2, 1}, PartsHeader: types.PartSetHeader{}} height, round := int64(10), 1 voteType := byte(types.PrevoteType) @@ -207,8 +207,8 @@ func TestSignProposal(t *testing.T) { privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) - block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{5, []byte{1, 2, 3}}} - block2 := types.BlockID{[]byte{3, 2, 1}, types.PartSetHeader{10, []byte{3, 2, 1}}} + block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{Total: 5, Hash: []byte{1, 2, 3}}} + block2 := types.BlockID{Hash: []byte{3, 2, 1}, PartsHeader: types.PartSetHeader{Total: 10, Hash: []byte{3, 2, 1}}} height, round := int64(10), 1 // sign a proposal for first time @@ -249,7 +249,7 @@ func TestDifferByTimestamp(t *testing.T) { privVal := GenFilePV(tempKeyFile.Name(), tempStateFile.Name()) - block1 := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{5, []byte{1, 2, 3}}} + block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{Total: 5, Hash: []byte{1, 2, 3}}} height, round := int64(10), 1 chainID := "mychainid" @@ -277,7 +277,7 @@ func TestDifferByTimestamp(t *testing.T) { // test vote { voteType := byte(types.PrevoteType) - blockID := types.BlockID{[]byte{1, 2, 3}, types.PartSetHeader{}} + blockID := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} vote := newVote(privVal.Key.Address, 0, height, round, voteType, blockID) err := privVal.SignVote("mychainid", vote) assert.NoError(t, err, "expected no error signing vote") diff --git a/state/execution_test.go b/state/execution_test.go index 6a23929b6..b2ee0a826 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -41,7 +41,7 @@ func TestApplyBlock(t *testing.T) { mock.Mempool{}, MockEvidencePool{}) block := makeBlock(state, 1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} //nolint:ineffassign state, err = blockExec.ApplyBlock(state, blockID, block) @@ -63,7 +63,7 @@ func TestBeginBlockValidators(t *testing.T) { prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} - prevBlockID := types.BlockID{prevHash, prevParts} + prevBlockID := types.BlockID{Hash: prevHash, PartsHeader: prevParts} now := tmtime.Now() commitSig0 := (&types.Vote{ValidatorIndex: 0, Timestamp: now, Type: types.PrecommitType}).CommitSig() @@ -116,7 +116,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} - prevBlockID := types.BlockID{prevHash, prevParts} + prevBlockID := types.BlockID{Hash: prevHash, PartsHeader: prevParts} height1, idx1, val1 := int64(8), 0, state.Validators.Validators[0].Address height2, idx2, val2 := int64(3), 1, state.Validators.Validators[1].Address @@ -160,7 +160,7 @@ func TestValidateValidatorUpdates(t *testing.T) { secpKey := secp256k1.GenPrivKey().PubKey() - defaultValidatorParams := types.ValidatorParams{[]string{types.ABCIPubKeyTypeEd25519}} + defaultValidatorParams := types.ValidatorParams{PubKeyTypes: []string{types.ABCIPubKeyTypeEd25519}} testCases := []struct { name string @@ -322,7 +322,7 @@ func TestEndBlockValidatorUpdates(t *testing.T) { require.NoError(t, err) block := makeBlock(state, 1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} pubkey := ed25519.GenPrivKey().PubKey() app.ValidatorUpdates = []abci.ValidatorUpdate{ @@ -370,7 +370,7 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) block := makeBlock(state, 1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} // Remove the only validator app.ValidatorUpdates = []abci.ValidatorUpdate{ @@ -399,10 +399,10 @@ func state(nVals, height int) (State, dbm.DB) { secret := []byte(fmt.Sprintf("test%d", i)) pk := ed25519.GenPrivKeyFromSecret(secret) vals[i] = types.GenesisValidator{ - pk.PubKey().Address(), - pk.PubKey(), - 1000, - fmt.Sprintf("test%d", i), + Address: pk.PubKey().Address(), + PubKey: pk.PubKey(), + Power: 1000, + Name: fmt.Sprintf("test%d", i), } } s, _ := MakeGenesisState(&types.GenesisDoc{ diff --git a/state/state_test.go b/state/state_test.go index eddbe255b..0f1806931 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -129,7 +129,7 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { {Code: 32, Data: []byte("Hello"), Log: "Huh?"}, }, types.ABCIResults{ - {32, []byte("Hello")}, + {Code: 32, Data: []byte("Hello")}, }}, 2: { []*abci.ResponseDeliverTx{ @@ -141,8 +141,8 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { }}, }, types.ABCIResults{ - {383, nil}, - {0, []byte("Gotcha!")}, + {Code: 383, Data: nil}, + {Code: 0, Data: []byte("Gotcha!")}, }}, 3: { nil, @@ -402,7 +402,7 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { assert.EqualValues(t, 0, val1.ProposerPriority) block := makeBlock(state, state.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} abciResponses := &ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } @@ -512,7 +512,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { assert.Equal(t, val1PubKey.Address(), state.Validators.Proposer.Address) block := makeBlock(state, state.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} // no updates: abciResponses := &ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, @@ -665,7 +665,7 @@ func TestLargeGenesisValidator(t *testing.T) { require.NoError(t, err) block := makeBlock(oldState, oldState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -691,7 +691,7 @@ func TestLargeGenesisValidator(t *testing.T) { EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -705,7 +705,7 @@ func TestLargeGenesisValidator(t *testing.T) { require.NoError(t, err) block := makeBlock(lastState, lastState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedStateInner, err := updateState(lastState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -736,7 +736,7 @@ func TestLargeGenesisValidator(t *testing.T) { EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{addedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} state, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) } @@ -748,7 +748,7 @@ func TestLargeGenesisValidator(t *testing.T) { EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal}}, } block = makeBlock(oldState, oldState.LastBlockHeight+1) - blockID = types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) updatedState, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) @@ -768,7 +768,7 @@ func TestLargeGenesisValidator(t *testing.T) { validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) block = makeBlock(curState, curState.LastBlockHeight+1) - blockID = types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} curState, err = updateState(curState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) if !bytes.Equal(curState.Validators.Proposer.Address, curState.NextValidators.Proposer.Address) { @@ -792,7 +792,7 @@ func TestLargeGenesisValidator(t *testing.T) { require.NoError(t, err) block := makeBlock(updatedState, updatedState.LastBlockHeight+1) - blockID := types.BlockID{block.Hash(), block.MakePartSet(testPartSize).Header()} + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} updatedState, err = updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) @@ -1031,7 +1031,7 @@ func makeHeaderPartsResponsesValPubKeyChange(state State, height int64, } } - return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses } func makeHeaderPartsResponsesValPowerChange(state State, height int64, @@ -1052,7 +1052,7 @@ func makeHeaderPartsResponsesValPowerChange(state State, height int64, } } - return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses } func makeHeaderPartsResponsesParams(state State, height int64, @@ -1062,7 +1062,7 @@ func makeHeaderPartsResponsesParams(state State, height int64, abciResponses := &ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(¶ms)}, } - return block.Header, types.BlockID{block.Hash(), types.PartSetHeader{}}, abciResponses + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses } type paramsChangeTestCase struct { diff --git a/state/tx_filter_test.go b/state/tx_filter_test.go index ffb41c178..e48ad2c3e 100644 --- a/state/tx_filter_test.go +++ b/state/tx_filter_test.go @@ -51,7 +51,7 @@ func randomGenesisDoc() *types.GenesisDoc { return &types.GenesisDoc{ GenesisTime: tmtime.Now(), ChainID: "abc", - Validators: []types.GenesisValidator{{pubkey.Address(), pubkey, 10, "myval"}}, + Validators: []types.GenesisValidator{{Address: pubkey.Address(), PubKey: pubkey, Power: 10, Name: "myval"}}, ConsensusParams: types.DefaultConsensusParams(), } } diff --git a/state/txindex/indexer_service_test.go b/state/txindex/indexer_service_test.go index 982d7b8c4..079f9cec2 100644 --- a/state/txindex/indexer_service_test.go +++ b/state/txindex/indexer_service_test.go @@ -43,14 +43,14 @@ func TestIndexerServiceIndexesBlocks(t *testing.T) { Tx: types.Tx("foo"), Result: abci.ResponseDeliverTx{Code: 0}, } - eventBus.PublishEventTx(types.EventDataTx{*txResult1}) + eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult1}) txResult2 := &types.TxResult{ Height: 1, Index: uint32(1), Tx: types.Tx("bar"), Result: abci.ResponseDeliverTx{Code: 0}, } - eventBus.PublishEventTx(types.EventDataTx{*txResult2}) + eventBus.PublishEventTx(types.EventDataTx{TxResult: *txResult2}) time.Sleep(100 * time.Millisecond) diff --git a/types/block.go b/types/block.go index 313eb6b75..55709ad60 100644 --- a/types/block.go +++ b/types/block.go @@ -198,7 +198,7 @@ func (b *Block) Hash() cmn.HexBytes { b.mtx.Lock() defer b.mtx.Unlock() - if b == nil || b.LastCommit == nil { + if b.LastCommit == nil { return nil } b.fillHeader() diff --git a/types/protobuf_test.go b/types/protobuf_test.go index 64caa3f4c..833c7dc38 100644 --- a/types/protobuf_test.go +++ b/types/protobuf_test.go @@ -90,7 +90,7 @@ func TestABCIHeader(t *testing.T) { height, numTxs, []byte("lastCommitHash"), []byte("dataHash"), []byte("evidenceHash"), ) - protocolVersion := version.Consensus{7, 8} + protocolVersion := version.Consensus{Block: 7, App: 8} timestamp := time.Now() lastBlockID := BlockID{ Hash: []byte("hash"), From a72cf5226848aad877c5d2fcdb721c6518ca1f4c Mon Sep 17 00:00:00 2001 From: needkane Date: Fri, 21 Jun 2019 19:18:49 +0800 Subject: [PATCH 129/211] abci/examples: switch from hex to base64 pubkey in kvstore (#3641) * abci/example: use base64 for update validator set * update kvstore/README.md * update CHANGELOG_PENDING.md and abci/example/kvstore/README.md --- CHANGELOG_PENDING.md | 6 +++--- abci/example/kvstore/README.md | 6 +++--- abci/example/kvstore/persistent_kvstore.go | 24 ++++++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 76c01bbbc..6fa55c0cb 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -29,9 +29,9 @@ ### FEATURES: ### IMPROVEMENTS: -- [p2p] \#3666 Add per channel telemtry to improve reactor observability - -* [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. +- [p2p] \#3666 Add per channel telemetry to improve reactor observability +- [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) +- [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) ### BUG FIXES: - [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) diff --git a/abci/example/kvstore/README.md b/abci/example/kvstore/README.md index e988eadb0..bed81a598 100644 --- a/abci/example/kvstore/README.md +++ b/abci/example/kvstore/README.md @@ -22,10 +22,10 @@ and the Handshake allows any necessary blocks to be replayed. Validator set changes are effected using the following transaction format: ``` -val:pubkey1/power1,addr2/power2,addr3/power3" +"val:pubkey1!power1,pubkey2!power2,pubkey3!power3" ``` -where `power1` is the new voting power for the validator with `pubkey1` (possibly a new one). +where `pubkeyN` is a base64-encoded 32-byte ed25519 key and `powerN` is a new voting power for the validator with `pubkeyN` (possibly a new one). +To remove a validator from the validator set, set power to `0`. There is no sybil protection against new validators joining. -Validators can be removed by setting their power to `0`. diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index b7484a4aa..ba0b53896 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -2,7 +2,7 @@ package kvstore import ( "bytes" - "encoding/hex" + "encoding/base64" "fmt" "strconv" "strings" @@ -60,10 +60,10 @@ func (app *PersistentKVStoreApplication) SetOption(req types.RequestSetOption) t return app.app.SetOption(req) } -// tx is either "val:pubkey/power" or "key=value" or just arbitrary bytes +// tx is either "val:pubkey!power" or "key=value" or just arbitrary bytes func (app *PersistentKVStoreApplication) DeliverTx(req types.RequestDeliverTx) types.ResponseDeliverTx { // if it starts with "val:", update the validator set - // format is "val:pubkey/power" + // format is "val:pubkey!power" if isValidatorTx(req.Tx) { // update validators in the merkle tree // and in app.ValUpdates @@ -129,33 +129,34 @@ func (app *PersistentKVStoreApplication) Validators() (validators []types.Valida } func MakeValSetChangeTx(pubkey types.PubKey, power int64) []byte { - return []byte(fmt.Sprintf("val:%X/%d", pubkey.Data, power)) + pubStr := base64.StdEncoding.EncodeToString(pubkey.Data) + return []byte(fmt.Sprintf("val:%s!%d", pubStr, power)) } func isValidatorTx(tx []byte) bool { return strings.HasPrefix(string(tx), ValidatorSetChangePrefix) } -// format is "val:pubkey/power" -// pubkey is raw 32-byte ed25519 key +// format is "val:pubkey!power" +// pubkey is a base64-encoded 32-byte ed25519 key func (app *PersistentKVStoreApplication) execValidatorTx(tx []byte) types.ResponseDeliverTx { tx = tx[len(ValidatorSetChangePrefix):] //get the pubkey and power - pubKeyAndPower := strings.Split(string(tx), "/") + pubKeyAndPower := strings.Split(string(tx), "!") if len(pubKeyAndPower) != 2 { return types.ResponseDeliverTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Expected 'pubkey/power'. Got %v", pubKeyAndPower)} + Log: fmt.Sprintf("Expected 'pubkey!power'. Got %v", pubKeyAndPower)} } pubkeyS, powerS := pubKeyAndPower[0], pubKeyAndPower[1] // decode the pubkey - pubkey, err := hex.DecodeString(pubkeyS) + pubkey, err := base64.StdEncoding.DecodeString(pubkeyS) if err != nil { return types.ResponseDeliverTx{ Code: code.CodeTypeEncodingError, - Log: fmt.Sprintf("Pubkey (%s) is invalid hex", pubkeyS)} + Log: fmt.Sprintf("Pubkey (%s) is invalid base64", pubkeyS)} } // decode the power @@ -176,9 +177,10 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate if v.Power == 0 { // remove validator if !app.app.state.db.Has(key) { + pubStr := base64.StdEncoding.EncodeToString(v.PubKey.Data) return types.ResponseDeliverTx{ Code: code.CodeTypeUnauthorized, - Log: fmt.Sprintf("Cannot remove non-existent validator %X", key)} + Log: fmt.Sprintf("Cannot remove non-existent validator %s", pubStr)} } app.app.state.db.Delete(key) } else { From e6cb96848497719880d74531c6b993ed8e2ecafe Mon Sep 17 00:00:00 2001 From: Ismail Khoffi Date: Fri, 21 Jun 2019 22:29:16 +0200 Subject: [PATCH 130/211] Prepare nuking develop (#3726) * Update Readme and contrib. guidelines: remove traces of master Signed-off-by: Ismail Khoffi * add simple example Signed-off-by: Ismail Khoffi * update contributing.md * add link * Update CONTRIBUTING.md * add a note on master and releases Signed-off-by: Ismail Khoffi * update readme --- CONTRIBUTING.md | 65 ++++++++++++++++++++++++++++++++----------------- README.md | 15 ++++++------ 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e68e6d1ee..77a625504 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,8 @@ Thank you for considering making contributions to Tendermint and related repositories! Start by taking a look at the [coding repo](https://github.com/tendermint/coding) for overall information on repository workflow and standards. -Please follow standard github best practices: fork the repo, branch from the tip of develop, make some commits, and submit a pull request to develop. See the [open issues](https://github.com/tendermint/tendermint/issues) for things we need help with! +Please follow standard github best practices: fork the repo, branch from the tip of `master`, make some commits, and submit a pull request to `master`. +See the [open issues](https://github.com/tendermint/tendermint/issues) for things we need help with! Before making a pull request, please open an issue describing the change you would like to make. If an issue for your change already exists, @@ -112,32 +113,36 @@ removed from the header in rpc responses as well. ## Branching Model and Release -We follow a variant of [git flow](http://nvie.com/posts/a-successful-git-branching-model/). -This means that all pull-requests should be made against develop. Any merge to -master constitutes a tagged release. +The main development branch is master. -Note all pull requests should be squash merged except for merging to master and -merging master back to develop. This keeps the commit history clean and makes it +Every release is maintained in a release branch named `vX.Y.Z`. + +Note all pull requests should be squash merged except for merging to a release branch (named `vX.Y`). This keeps the commit history clean and makes it easy to reference the pull request where a change was introduced. -### Development Procedure: -- the latest state of development is on `develop` -- `develop` must never fail `make test` -- never --force onto `develop` (except when reverting a broken commit, which should seldom happen) +### Development Procedure + +- the latest state of development is on `master` +- `master` must never fail `make test` +- never --force onto `master` (except when reverting a broken commit, which should seldom happen) - create a development branch either on github.com/tendermint/tendermint, or your fork (using `git remote add origin`) - make changes and update the `CHANGELOG_PENDING.md` to record your change -- before submitting a pull request, run `git rebase` on top of the latest `develop` +- before submitting a pull request, run `git rebase` on top of the latest `master` -### Pull Merge Procedure: -- ensure pull branch is based on a recent develop +### Pull Merge Procedure + +- ensure pull branch is based on a recent `master` - run `make test` to ensure that all tests pass - squash merge pull request - the `unstable` branch may be used to aggregate pull merges before fixing tests -### Release Procedure: -- start on `develop` -- run integration tests (see `test_integrations` in Makefile) -- prepare release in a pull request against develop (to be squash merged): +### Release Procedure + +#### Major Release + +1. start on `master` +2. run integration tests (see `test_integrations` in Makefile) +3. prepare release in a pull request against `master` (to be squash merged): - copy `CHANGELOG_PENDING.md` to top of `CHANGELOG.md` - run `python ./scripts/linkify_changelog.py CHANGELOG.md` to add links for all issues @@ -147,14 +152,28 @@ easy to reference the pull request where a change was introduced. ./scripts/authors.sh ` - reset the `CHANGELOG_PENDING.md` - bump versions -- push latest develop with prepared release details to release/vX.X.X to run the extended integration tests on the CI -- if necessary, make pull requests against release/vX.X.X and squash merge them -- merge to master (don't squash merge!) -- merge master back to develop (don't squash merge!) +4. push your changes with prepared release details to `vX.X` (this will trigger the release `vX.X.0`) +5. merge back to master (don't squash merge!) + +#### Minor Release + +If there were no breaking changes and you need to create a release nonetheless, +the procedure is almost exactly like with a new release above. + +The only difference is that in the end you create a pull request against the existing `X.X` branch. +The branch name should match the release number you want to create. +Merging this PR will trigger the next release. +For example, if the PR is against an existing 0.34 branch which already contains a v0.34.0 release/tag, +the patch version will be incremented and the created release will be v0.34.1. -### Hotfix Procedure: +#### Backport Release -- follow the normal development and release procedure without any differences +1. start from the existing release branch you want to backport changes to (e.g. v0.30) +Branch to a release/vX.X.X branch locally (e.g. release/v0.30.7) +2. cherry pick the commit(s) that contain the changes you want to backport (usually these commits are from squash-merged PRs which were already reviewed) +3. steps 2 and 3 from [Major Release](#major-release) +4. push changes to release/vX.X.X branch +5. open a PR against the existing vX.X branch ## Testing diff --git a/README.md b/README.md index ad4fc1308..ec7b28cc0 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,6 @@ https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/6874 Branch | Tests | Coverage ----------|-------|---------- master | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/master.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/master) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/master/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) -develop | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/develop.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/develop) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/develop/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine - written in any programming language - and securely replicates it on many machines. @@ -27,13 +26,15 @@ For protocol details, see [the specification](/docs/spec). For detailed analysis of the consensus protocol, including safety and liveness proofs, see our recent paper, "[The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938)". -## A Note on Production Readiness +## Releases -While Tendermint is being used in production in private, permissioned -environments, we are still working actively to harden and audit it in preparation -for use in public blockchains, such as the [Cosmos Network](https://cosmos.network/). -We are also still making breaking changes to the protocol and the APIs. -Thus, we tag the releases as *alpha software*. +NOTE: The master branch is now an active development branch (starting with `v0.32`). Please, do not depend on it and +use [releases](https://github.com/tendermint/tendermint/releases) instead. + +Tendermint is being used in production in both private and public environments, +most notably the blockchains of the [Cosmos Network](https://cosmos.network/). +However, we are still making breaking changes to the protocol and the APIs and have not yet released v1.0. +See below for more details about [versioning](#versioning). In any case, if you intend to run Tendermint in production, please [contact us](mailto:partners@tendermint.com) and [join the chat](https://riot.im/app/#/room/#tendermint:matrix.org). From 19fbfb57e325c0108c2108d3b4542e8f68a24d34 Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Fri, 21 Jun 2019 17:29:29 -0400 Subject: [PATCH 131/211] state: add more tests for block validation (#3674) * Expose priv validators for use in testing * Generalize block header validation test past height 1 * Remove ineffectual assignment * Remove redundant SaveState call * Reorder comment for clarity * Use the block executor ApplyBlock function instead of implementing a stripped-down version of it * Remove commented-out code * Remove unnecessary test The required tests already appear to be implemented (implicitly) through the TestValidateBlockHeader test. * Allow for catching of specific error types during TestValidateBlockCommit * Make return error testable * Clean up and add TestValidateBlockCommit code * Fix formatting * Extract function to create a new mock test app * Update comment for clarity * Fix comment * Add skeleton code for evidence-related test * Allow for addressing priv val by address * Generalize test beyond a single validator * Generalize TestValidateBlockEvidence past first height * Reorder code to clearly separate tests and utility code * Use a common constant for stop height for testing in state/validation_test.go * Refactor errors to resemble existing conventions * Fix formatting * Extract common helper functions Having the tests littered with helper functions makes them less easily readable imho, so I've pulled them out into a separate file. This also makes it easier to see what helper functions are available during testing, so we minimize the chance of duplication when writing new tests. * Remove unused parameter * Remove unused parameters * Add field keys * Remove unused height constant * Fix typo * Fix incorrect return error * Add field keys * Use separate package for tests This refactors all of the state package's tests into a state_test package, so as to keep any usage of the state package's internal methods explicit. Any internal methods/constants used by tests are now explicitly exported in state/export_test.go * Refactor: extract helper function to make, validate, execute and commit a block * Rename state function to makeState * Remove redundant constant for number of validators * Refactor mock evidence registration into TestMain * Remove extraneous nVals variable * Replace function-level TODOs with file-level TODO and explanation * Remove extraneous comment * Fix linting issues brought up by GolangCI (pulled in from latest merge from develop) --- state/execution_test.go | 118 ++--------------- state/export_test.go | 62 +++++++++ state/helpers_test.go | 280 +++++++++++++++++++++++++++++++++++++++ state/main_test.go | 13 ++ state/services.go | 2 +- state/state_test.go | 217 +++++++++--------------------- state/store_test.go | 31 ++--- state/tx_filter_test.go | 19 +-- state/validation.go | 5 +- state/validation_test.go | 221 ++++++++++++++++++------------ types/errors.go | 41 ++++++ types/validator_set.go | 4 +- 12 files changed, 639 insertions(+), 374 deletions(-) create mode 100644 state/export_test.go create mode 100644 state/helpers_test.go create mode 100644 state/main_test.go create mode 100644 types/errors.go diff --git a/state/execution_test.go b/state/execution_test.go index b2ee0a826..38301df73 100644 --- a/state/execution_test.go +++ b/state/execution_test.go @@ -1,23 +1,20 @@ -package state +package state_test import ( "context" - "fmt" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/abci/example/kvstore" abci "github.com/tendermint/tendermint/abci/types" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/secp256k1" - cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" ) @@ -35,10 +32,10 @@ func TestApplyBlock(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(1, 1) + state, stateDB, _ := makeState(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), - mock.Mempool{}, MockEvidencePool{}) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), + mock.Mempool{}, sm.MockEvidencePool{}) block := makeBlock(state, 1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} @@ -59,7 +56,7 @@ func TestBeginBlockValidators(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(2, 2) + state, stateDB, _ := makeState(2, 2) prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} @@ -85,7 +82,7 @@ func TestBeginBlockValidators(t *testing.T) { // block for height 2 block, _ := state.MakeBlock(2, makeTxs(2), lastCommit, nil, state.Validators.GetProposer().Address) - _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) + _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) require.Nil(t, err, tc.desc) // -> app receives a list of validators with a bool indicating if they signed @@ -112,7 +109,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(2, 12) + state, stateDB, _ := makeState(2, 12) prevHash := state.LastBlockID.Hash prevParts := types.PartSetHeader{} @@ -146,7 +143,7 @@ func TestBeginBlockByzantineValidators(t *testing.T) { block, _ := state.MakeBlock(10, makeTxs(2), lastCommit, nil, state.Validators.GetProposer().Address) block.Time = now block.Evidence.Evidence = tc.evidence - _, err = ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) + _, err = sm.ExecCommitBlock(proxyApp.Consensus(), block, log.TestingLogger(), stateDB) require.Nil(t, err, tc.desc) // -> app must receive an index of the byzantine validator @@ -214,7 +211,7 @@ func TestValidateValidatorUpdates(t *testing.T) { for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { - err := validateValidatorUpdates(tc.abciUpdates, tc.validatorParams) + err := sm.ValidateValidatorUpdates(tc.abciUpdates, tc.validatorParams) if tc.shouldErr { assert.Error(t, err) } else { @@ -308,9 +305,9 @@ func TestEndBlockValidatorUpdates(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(1, 1) + state, stateDB, _ := makeState(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) eventBus := types.NewEventBus() err = eventBus.Start() @@ -366,8 +363,8 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { require.Nil(t, err) defer proxyApp.Stop() - state, stateDB := state(1, 1) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, MockEvidencePool{}) + state, stateDB, _ := makeState(1, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) block := makeBlock(state, 1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} @@ -382,90 +379,3 @@ func TestEndBlockValidatorUpdatesResultingInEmptySet(t *testing.T) { assert.NotEmpty(t, state.NextValidators.Validators) } - -//---------------------------------------------------------------------------- - -// make some bogus txs -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < nTxsPerBlock; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func state(nVals, height int) (State, dbm.DB) { - vals := make([]types.GenesisValidator, nVals) - for i := 0; i < nVals; i++ { - secret := []byte(fmt.Sprintf("test%d", i)) - pk := ed25519.GenPrivKeyFromSecret(secret) - vals[i] = types.GenesisValidator{ - Address: pk.PubKey().Address(), - PubKey: pk.PubKey(), - Power: 1000, - Name: fmt.Sprintf("test%d", i), - } - } - s, _ := MakeGenesisState(&types.GenesisDoc{ - ChainID: chainID, - Validators: vals, - AppHash: nil, - }) - - // save validators to db for 2 heights - stateDB := dbm.NewMemDB() - SaveState(stateDB, s) - - for i := 1; i < height; i++ { - s.LastBlockHeight++ - s.LastValidators = s.Validators.Copy() - SaveState(stateDB, s) - } - return s, stateDB -} - -func makeBlock(state State, height int64) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(state.LastBlockHeight), new(types.Commit), nil, state.Validators.GetProposer().Address) - return block -} - -//---------------------------------------------------------------------------- - -type testApp struct { - abci.BaseApplication - - CommitVotes []abci.VoteInfo - ByzantineValidators []abci.Evidence - ValidatorUpdates []abci.ValidatorUpdate -} - -var _ abci.Application = (*testApp)(nil) - -func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) { - return abci.ResponseInfo{} -} - -func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { - app.CommitVotes = req.LastCommitInfo.Votes - app.ByzantineValidators = req.ByzantineValidators - return abci.ResponseBeginBlock{} -} - -func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { - return abci.ResponseEndBlock{ValidatorUpdates: app.ValidatorUpdates} -} - -func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { - return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} -} - -func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { - return abci.ResponseCheckTx{} -} - -func (app *testApp) Commit() abci.ResponseCommit { - return abci.ResponseCommit{} -} - -func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { - return -} diff --git a/state/export_test.go b/state/export_test.go new file mode 100644 index 000000000..af7f5cc23 --- /dev/null +++ b/state/export_test.go @@ -0,0 +1,62 @@ +package state + +import ( + abci "github.com/tendermint/tendermint/abci/types" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/types" +) + +// +// TODO: Remove dependence on all entities exported from this file. +// +// Every entity exported here is dependent on a private entity from the `state` +// package. Currently, these functions are only made available to tests in the +// `state_test` package, but we should not be relying on them for our testing. +// Instead, we should be exclusively relying on exported entities for our +// testing, and should be refactoring exported entities to make them more +// easily testable from outside of the package. +// + +const ValSetCheckpointInterval = valSetCheckpointInterval + +// UpdateState is an alias for updateState exported from execution.go, +// exclusively and explicitly for testing. +func UpdateState( + state State, + blockID types.BlockID, + header *types.Header, + abciResponses *ABCIResponses, + validatorUpdates []*types.Validator, +) (State, error) { + return updateState(state, blockID, header, abciResponses, validatorUpdates) +} + +// ValidateValidatorUpdates is an alias for validateValidatorUpdates exported +// from execution.go, exclusively and explicitly for testing. +func ValidateValidatorUpdates(abciUpdates []abci.ValidatorUpdate, params types.ValidatorParams) error { + return validateValidatorUpdates(abciUpdates, params) +} + +// CalcValidatorsKey is an alias for the private calcValidatorsKey method in +// store.go, exported exclusively and explicitly for testing. +func CalcValidatorsKey(height int64) []byte { + return calcValidatorsKey(height) +} + +// SaveABCIResponses is an alias for the private saveABCIResponses method in +// store.go, exported exclusively and explicitly for testing. +func SaveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { + saveABCIResponses(db, height, abciResponses) +} + +// SaveConsensusParamsInfo is an alias for the private saveConsensusParamsInfo +// method in store.go, exported exclusively and explicitly for testing. +func SaveConsensusParamsInfo(db dbm.DB, nextHeight, changeHeight int64, params types.ConsensusParams) { + saveConsensusParamsInfo(db, nextHeight, changeHeight, params) +} + +// SaveValidatorsInfo is an alias for the private saveValidatorsInfo method in +// store.go, exported exclusively and explicitly for testing. +func SaveValidatorsInfo(db dbm.DB, height, lastHeightChanged int64, valSet *types.ValidatorSet) { + saveValidatorsInfo(db, height, lastHeightChanged, valSet) +} diff --git a/state/helpers_test.go b/state/helpers_test.go new file mode 100644 index 000000000..e8cb27585 --- /dev/null +++ b/state/helpers_test.go @@ -0,0 +1,280 @@ +package state_test + +import ( + "bytes" + "fmt" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +type paramsChangeTestCase struct { + height int64 + params types.ConsensusParams +} + +// always returns true if asked if any evidence was already committed. +type mockEvPoolAlwaysCommitted struct{} + +func (m mockEvPoolAlwaysCommitted) PendingEvidence(int64) []types.Evidence { return nil } +func (m mockEvPoolAlwaysCommitted) AddEvidence(types.Evidence) error { return nil } +func (m mockEvPoolAlwaysCommitted) Update(*types.Block, sm.State) {} +func (m mockEvPoolAlwaysCommitted) IsCommitted(types.Evidence) bool { return true } + +func newTestApp() proxy.AppConns { + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + return proxy.NewAppConns(cc) +} + +func makeAndCommitGoodBlock( + 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) { + // A good block passes + state, blockID, err := makeAndApplyGoodBlock(state, height, lastCommit, proposerAddr, blockExec, evidence) + if err != nil { + return state, types.BlockID{}, nil, err + } + + // Simulate a lastCommit for this block from all validators for the next height + commit, err := makeValidCommit(height, blockID, state.Validators, privVals) + if err != nil { + return state, types.BlockID{}, nil, err + } + 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) { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, evidence, proposerAddr) + if err := blockExec.ValidateBlock(state, block); err != nil { + return state, types.BlockID{}, err + } + blockID := types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}} + state, err := blockExec.ApplyBlock(state, blockID, block) + if err != nil { + return state, types.BlockID{}, err + } + return state, blockID, nil +} + +func makeVote(height int64, blockID types.BlockID, valSet *types.ValidatorSet, privVal types.PrivValidator) (*types.Vote, error) { + addr := privVal.GetPubKey().Address() + idx, _ := valSet.GetByAddress(addr) + vote := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: height, + Round: 0, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + if err := privVal.SignVote(chainID, vote); err != nil { + return nil, err + } + return vote, nil +} + +func makeValidCommit(height int64, blockID types.BlockID, vals *types.ValidatorSet, privVals map[string]types.PrivValidator) (*types.Commit, error) { + sigs := make([]*types.CommitSig, 0) + for i := 0; i < vals.Size(); i++ { + _, val := vals.GetByIndex(i) + vote, err := makeVote(height, blockID, vals, privVals[val.Address.String()]) + if err != nil { + return nil, err + } + sigs = append(sigs, vote.CommitSig()) + } + return types.NewCommit(blockID, sigs), nil +} + +// make some bogus txs +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < nTxsPerBlock; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeState(nVals, height int) (sm.State, dbm.DB, map[string]types.PrivValidator) { + vals := make([]types.GenesisValidator, nVals) + privVals := make(map[string]types.PrivValidator, nVals) + for i := 0; i < nVals; i++ { + secret := []byte(fmt.Sprintf("test%d", i)) + pk := ed25519.GenPrivKeyFromSecret(secret) + valAddr := pk.PubKey().Address() + vals[i] = types.GenesisValidator{ + Address: valAddr, + PubKey: pk.PubKey(), + Power: 1000, + Name: fmt.Sprintf("test%d", i), + } + privVals[valAddr.String()] = types.NewMockPVWithParams(pk, false, false) + } + s, _ := sm.MakeGenesisState(&types.GenesisDoc{ + ChainID: chainID, + Validators: vals, + AppHash: nil, + }) + + stateDB := dbm.NewMemDB() + sm.SaveState(stateDB, s) + + for i := 1; i < height; i++ { + s.LastBlockHeight++ + s.LastValidators = s.Validators.Copy() + sm.SaveState(stateDB, s) + } + return s, stateDB, privVals +} + +func makeBlock(state sm.State, height int64) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(state.LastBlockHeight), new(types.Commit), nil, state.Validators.GetProposer().Address) + return block +} + +func genValSet(size int) *types.ValidatorSet { + vals := make([]*types.Validator, size) + for i := 0; i < size; i++ { + vals[i] = types.NewValidator(ed25519.GenPrivKey().PubKey(), 10) + } + return types.NewValidatorSet(vals) +} + +func makeConsensusParams( + blockBytes, blockGas int64, + blockTimeIotaMs int64, + evidenceAge int64, +) types.ConsensusParams { + return types.ConsensusParams{ + Block: types.BlockParams{ + MaxBytes: blockBytes, + MaxGas: blockGas, + TimeIotaMs: blockTimeIotaMs, + }, + Evidence: types.EvidenceParams{ + MaxAge: evidenceAge, + }, + } +} + +func makeHeaderPartsResponsesValPubKeyChange(state sm.State, pubkey crypto.PubKey) (types.Header, types.BlockID, *sm.ABCIResponses) { + + block := makeBlock(state, state.LastBlockHeight+1) + abciResponses := &sm.ABCIResponses{ + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, + } + + // If the pubkey is new, remove the old and add the new. + _, val := state.NextValidators.GetByIndex(0) + if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) { + abciResponses.EndBlock = &abci.ResponseEndBlock{ + ValidatorUpdates: []abci.ValidatorUpdate{ + types.TM2PB.NewValidatorUpdate(val.PubKey, 0), + types.TM2PB.NewValidatorUpdate(pubkey, 10), + }, + } + } + + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses +} + +func makeHeaderPartsResponsesValPowerChange(state sm.State, power int64) (types.Header, types.BlockID, *sm.ABCIResponses) { + + block := makeBlock(state, state.LastBlockHeight+1) + abciResponses := &sm.ABCIResponses{ + EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, + } + + // If the pubkey is new, remove the old and add the new. + _, val := state.NextValidators.GetByIndex(0) + if val.VotingPower != power { + abciResponses.EndBlock = &abci.ResponseEndBlock{ + ValidatorUpdates: []abci.ValidatorUpdate{ + types.TM2PB.NewValidatorUpdate(val.PubKey, power), + }, + } + } + + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses +} + +func makeHeaderPartsResponsesParams(state sm.State, params types.ConsensusParams) (types.Header, types.BlockID, *sm.ABCIResponses) { + + block := makeBlock(state, state.LastBlockHeight+1) + abciResponses := &sm.ABCIResponses{ + EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(¶ms)}, + } + return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses +} + +func randomGenesisDoc() *types.GenesisDoc { + pubkey := ed25519.GenPrivKey().PubKey() + return &types.GenesisDoc{ + GenesisTime: tmtime.Now(), + ChainID: "abc", + Validators: []types.GenesisValidator{ + { + Address: pubkey.Address(), + PubKey: pubkey, + Power: 10, + Name: "myval", + }, + }, + ConsensusParams: types.DefaultConsensusParams(), + } +} + +//---------------------------------------------------------------------------- + +type testApp struct { + abci.BaseApplication + + CommitVotes []abci.VoteInfo + ByzantineValidators []abci.Evidence + ValidatorUpdates []abci.ValidatorUpdate +} + +var _ abci.Application = (*testApp)(nil) + +func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) { + return abci.ResponseInfo{} +} + +func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { + app.CommitVotes = req.LastCommitInfo.Votes + app.ByzantineValidators = req.ByzantineValidators + return abci.ResponseBeginBlock{} +} + +func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { + return abci.ResponseEndBlock{ValidatorUpdates: app.ValidatorUpdates} +} + +func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { + return abci.ResponseDeliverTx{Events: []abci.Event{}} +} + +func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { + return abci.ResponseCheckTx{} +} + +func (app *testApp) Commit() abci.ResponseCommit { + return abci.ResponseCommit{} +} + +func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { + return +} diff --git a/state/main_test.go b/state/main_test.go new file mode 100644 index 000000000..00ecf2686 --- /dev/null +++ b/state/main_test.go @@ -0,0 +1,13 @@ +package state_test + +import ( + "os" + "testing" + + "github.com/tendermint/tendermint/types" +) + +func TestMain(m *testing.M) { + types.RegisterMockEvidencesGlobal() + os.Exit(m.Run()) +} diff --git a/state/services.go b/state/services.go index 98f6afce3..10b389ee7 100644 --- a/state/services.go +++ b/state/services.go @@ -43,7 +43,7 @@ type EvidencePool interface { IsCommitted(types.Evidence) bool } -// MockEvidencePool is an empty implementation of a Mempool, useful for testing. +// MockEvidencePool is an empty implementation of EvidencePool, useful for testing. type MockEvidencePool struct{} func (m MockEvidencePool) PendingEvidence(int64) []types.Evidence { return nil } diff --git a/state/state_test.go b/state/state_test.go index 0f1806931..caba188e8 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -1,4 +1,4 @@ -package state +package state_test import ( "bytes" @@ -10,23 +10,22 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" + sm "github.com/tendermint/tendermint/state" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/types" ) // setupTestCase does setup common to all test cases. -func setupTestCase(t *testing.T) (func(t *testing.T), dbm.DB, State) { +func setupTestCase(t *testing.T) (func(t *testing.T), dbm.DB, sm.State) { config := cfg.ResetTestRoot("state_") dbType := dbm.DBBackendType(config.DBBackend) stateDB := dbm.NewDB("state", dbType, config.DBDir()) - state, err := LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) + state, err := sm.LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) assert.NoError(t, err, "expected no error on LoadStateFromDBOrGenesisFile") tearDown := func(t *testing.T) { os.RemoveAll(config.RootDir) } @@ -59,7 +58,7 @@ func TestMakeGenesisStateNilValidators(t *testing.T) { Validators: nil, } require.Nil(t, doc.ValidateAndComplete()) - state, err := MakeGenesisState(&doc) + state, err := sm.MakeGenesisState(&doc) require.Nil(t, err) require.Equal(t, 0, len(state.Validators.Validators)) require.Equal(t, 0, len(state.NextValidators.Validators)) @@ -73,9 +72,9 @@ func TestStateSaveLoad(t *testing.T) { assert := assert.New(t) state.LastBlockHeight++ - SaveState(stateDB, state) + sm.SaveState(stateDB, state) - loadedState := LoadState(stateDB) + loadedState := sm.LoadState(stateDB) assert.True(state.Equals(loadedState), fmt.Sprintf("expected state and its copy to be identical.\ngot: %v\nexpected: %v\n", loadedState, state)) @@ -99,8 +98,8 @@ func TestABCIResponsesSaveLoad1(t *testing.T) { types.TM2PB.NewValidatorUpdate(ed25519.GenPrivKey().PubKey(), 10), }} - saveABCIResponses(stateDB, block.Height, abciResponses) - loadedABCIResponses, err := LoadABCIResponses(stateDB, block.Height) + sm.SaveABCIResponses(stateDB, block.Height, abciResponses) + loadedABCIResponses, err := sm.LoadABCIResponses(stateDB, block.Height) assert.Nil(err) assert.Equal(abciResponses, loadedABCIResponses, fmt.Sprintf("ABCIResponses don't match:\ngot: %v\nexpected: %v\n", @@ -153,24 +152,24 @@ func TestABCIResponsesSaveLoad2(t *testing.T) { // Query all before, this should return error. for i := range cases { h := int64(i + 1) - res, err := LoadABCIResponses(stateDB, h) + res, err := sm.LoadABCIResponses(stateDB, h) assert.Error(err, "%d: %#v", i, res) } // Add all cases. for i, tc := range cases { h := int64(i + 1) // last block height, one below what we save - responses := &ABCIResponses{ + responses := &sm.ABCIResponses{ DeliverTx: tc.added, EndBlock: &abci.ResponseEndBlock{}, } - saveABCIResponses(stateDB, h, responses) + sm.SaveABCIResponses(stateDB, h, responses) } // Query all before, should return expected value. for i, tc := range cases { h := int64(i + 1) - res, err := LoadABCIResponses(stateDB, h) + res, err := sm.LoadABCIResponses(stateDB, h) assert.NoError(err, "%d", i) assert.Equal(tc.expected.Hash(), res.ResultsHash(), "%d", i) } @@ -184,26 +183,26 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { assert := assert.New(t) // Can't load anything for height 0. - v, err := LoadValidators(stateDB, 0) - assert.IsType(ErrNoValSetForHeight{}, err, "expected err at height 0") + v, err := sm.LoadValidators(stateDB, 0) + assert.IsType(sm.ErrNoValSetForHeight{}, err, "expected err at height 0") // Should be able to load for height 1. - v, err = LoadValidators(stateDB, 1) + v, err = sm.LoadValidators(stateDB, 1) assert.Nil(err, "expected no err at height 1") assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match") // Should be able to load for height 2. - v, err = LoadValidators(stateDB, 2) + v, err = sm.LoadValidators(stateDB, 2) assert.Nil(err, "expected no err at height 2") assert.Equal(v.Hash(), state.NextValidators.Hash(), "expected validator hashes to match") // Increment height, save; should be able to load for next & next next height. state.LastBlockHeight++ nextHeight := state.LastBlockHeight + 1 - saveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) - vp0, err := LoadValidators(stateDB, nextHeight+0) + sm.SaveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) + vp0, err := sm.LoadValidators(stateDB, nextHeight+0) assert.Nil(err, "expected no err") - vp1, err := LoadValidators(stateDB, nextHeight+1) + vp1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(err, "expected no err") assert.Equal(vp0.Hash(), state.Validators.Hash(), "expected validator hashes to match") assert.Equal(vp1.Hash(), state.NextValidators.Hash(), "expected next validator hashes to match") @@ -232,13 +231,13 @@ func TestOneValidatorChangesSaveLoad(t *testing.T) { changeIndex++ power++ } - header, blockID, responses := makeHeaderPartsResponsesValPowerChange(state, i, power) + header, blockID, responses := makeHeaderPartsResponsesValPowerChange(state, power) validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates) require.NoError(t, err) - state, err = updateState(state, blockID, &header, responses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates) require.NoError(t, err) nextHeight := state.LastBlockHeight + 1 - saveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) + sm.SaveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) } // On each height change, increment the power by one. @@ -256,7 +255,7 @@ func TestOneValidatorChangesSaveLoad(t *testing.T) { } for i, power := range testCases { - v, err := LoadValidators(stateDB, int64(i+1+1)) // +1 because vset changes delayed by 1 block. + v, err := sm.LoadValidators(stateDB, int64(i+1+1)) // +1 because vset changes delayed by 1 block. assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", i)) assert.Equal(t, v.Size(), 1, "validator set size is greater than 1: %d", v.Size()) _, val := v.GetByIndex(0) @@ -403,12 +402,12 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { block := makeBlock(state, state.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err := updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) curTotal := val1VotingPower // one increment step and one validator: 0 + power - total_power == 0 @@ -420,7 +419,7 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { updateAddVal := abci.ValidatorUpdate{PubKey: types.TM2PB.PubKey(val2PubKey), Power: val2VotingPower} validatorUpdates, err = types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{updateAddVal}) assert.NoError(t, err) - updatedState2, err := updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState2, err := sm.UpdateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) require.Equal(t, len(updatedState2.NextValidators.Validators), 2) @@ -459,7 +458,7 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { // this will cause the diff of priorities (77) // to be larger than threshold == 2*totalVotingPower (22): - updatedState3, err := updateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState3, err := sm.UpdateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) require.Equal(t, len(updatedState3.NextValidators.Validators), 2) @@ -514,13 +513,13 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { block := makeBlock(state, state.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err := updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) // 0 + 10 (initial prio) - 10 (avg) - 10 (mostest - total) = -10 @@ -535,7 +534,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { validatorUpdates, err = types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{updateAddVal}) assert.NoError(t, err) - updatedState2, err := updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState2, err := sm.UpdateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) require.Equal(t, len(updatedState2.NextValidators.Validators), 2) @@ -572,7 +571,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState3, err := updateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState3, err := sm.UpdateState(updatedState2, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) assert.Equal(t, updatedState3.Validators.Proposer.Address, updatedState3.NextValidators.Proposer.Address) @@ -596,13 +595,13 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { // no changes in voting power and both validators have same voting power // -> proposers should alternate: oldState := updatedState3 - abciResponses = &ABCIResponses{ + abciResponses = &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - oldState, err = updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + oldState, err = sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) expectedVal1Prio2 = 1 expectedVal2Prio2 = -1 @@ -611,13 +610,13 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { for i := 0; i < 1000; i++ { // no validator updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) assert.NoError(t, err) // alternate (and cyclic priorities): assert.NotEqual(t, updatedState.Validators.Proposer.Address, updatedState.NextValidators.Proposer.Address, "iter: %v", i) @@ -658,7 +657,7 @@ func TestLargeGenesisValidator(t *testing.T) { oldState := state for i := 0; i < 10; i++ { // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) @@ -667,7 +666,7 @@ func TestLargeGenesisValidator(t *testing.T) { block := makeBlock(oldState, oldState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) // no changes in voting power (ProposerPrio += VotingPower == Voting in 1st round; than shiftByAvg == 0, // than -Total == -Voting) @@ -687,18 +686,18 @@ func TestLargeGenesisValidator(t *testing.T) { firstAddedVal := abci.ValidatorUpdate{PubKey: types.TM2PB.PubKey(firstAddedValPubKey), Power: firstAddedValVotingPower} validatorUpdates, err := types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{firstAddedVal}) assert.NoError(t, err) - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{firstAddedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedState, err := updateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err := sm.UpdateState(oldState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) lastState := updatedState for i := 0; i < 200; i++ { // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) @@ -707,7 +706,7 @@ func TestLargeGenesisValidator(t *testing.T) { block := makeBlock(lastState, lastState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedStateInner, err := updateState(lastState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedStateInner, err := sm.UpdateState(lastState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) lastState = updatedStateInner } @@ -732,26 +731,26 @@ func TestLargeGenesisValidator(t *testing.T) { validatorUpdates, err := types.PB2TM.ValidatorUpdates([]abci.ValidatorUpdate{addedVal}) assert.NoError(t, err) - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{addedVal}}, } block := makeBlock(oldState, oldState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - state, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) } require.Equal(t, 10+2, len(state.NextValidators.Validators)) // remove genesis validator: removeGenesisVal := abci.ValidatorUpdate{PubKey: types.TM2PB.PubKey(genesisPubKey), Power: 0} - abciResponses = &ABCIResponses{ + abciResponses = &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{removeGenesisVal}}, } block = makeBlock(oldState, oldState.LastBlockHeight+1) blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) - updatedState, err = updateState(state, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err = sm.UpdateState(state, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) // only the first added val (not the genesis val) should be left assert.Equal(t, 11, len(updatedState.NextValidators.Validators)) @@ -762,14 +761,14 @@ func TestLargeGenesisValidator(t *testing.T) { count := 0 isProposerUnchanged := true for isProposerUnchanged { - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err = types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) require.NoError(t, err) block = makeBlock(curState, curState.LastBlockHeight+1) blockID = types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - curState, err = updateState(curState, blockID, &block.Header, abciResponses, validatorUpdates) + curState, err = sm.UpdateState(curState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) if !bytes.Equal(curState.Validators.Proposer.Address, curState.NextValidators.Proposer.Address) { isProposerUnchanged = false @@ -785,7 +784,7 @@ func TestLargeGenesisValidator(t *testing.T) { proposers := make([]*types.Validator, numVals) for i := 0; i < 100; i++ { // no updates: - abciResponses := &ABCIResponses{ + abciResponses := &sm.ABCIResponses{ EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, } validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciResponses.EndBlock.ValidatorUpdates) @@ -794,7 +793,7 @@ func TestLargeGenesisValidator(t *testing.T) { block := makeBlock(updatedState, updatedState.LastBlockHeight+1) blockID := types.BlockID{Hash: block.Hash(), PartsHeader: block.MakePartSet(testPartSize).Header()} - updatedState, err = updateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) + updatedState, err = sm.UpdateState(updatedState, blockID, &block.Header, abciResponses, validatorUpdates) require.NoError(t, err) if i > numVals { // expect proposers to cycle through after the first iteration (of numVals blocks): if proposers[i%numVals] == nil { @@ -812,15 +811,15 @@ func TestStoreLoadValidatorsIncrementsProposerPriority(t *testing.T) { defer tearDown(t) state.Validators = genValSet(valSetSize) state.NextValidators = state.Validators.CopyIncrementProposerPriority(1) - SaveState(stateDB, state) + sm.SaveState(stateDB, state) nextHeight := state.LastBlockHeight + 1 - v0, err := LoadValidators(stateDB, nextHeight) + v0, err := sm.LoadValidators(stateDB, nextHeight) assert.Nil(t, err) acc0 := v0.Validators[0].ProposerPriority - v1, err := LoadValidators(stateDB, nextHeight+1) + v1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(t, err) acc1 := v1.Validators[0].ProposerPriority @@ -836,28 +835,27 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { require.Equal(t, int64(0), state.LastBlockHeight) state.Validators = genValSet(valSetSize) state.NextValidators = state.Validators.CopyIncrementProposerPriority(1) - SaveState(stateDB, state) + sm.SaveState(stateDB, state) _, valOld := state.Validators.GetByIndex(0) var pubkeyOld = valOld.PubKey pubkey := ed25519.GenPrivKey().PubKey() - const height = 1 // Swap the first validator with a new one (validator set size stays the same). - header, blockID, responses := makeHeaderPartsResponsesValPubKeyChange(state, height, pubkey) + header, blockID, responses := makeHeaderPartsResponsesValPubKeyChange(state, pubkey) // Save state etc. var err error var validatorUpdates []*types.Validator validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates) require.NoError(t, err) - state, err = updateState(state, blockID, &header, responses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates) require.Nil(t, err) nextHeight := state.LastBlockHeight + 1 - saveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) + sm.SaveValidatorsInfo(stateDB, nextHeight+1, state.LastHeightValidatorsChanged, state.NextValidators) // Load nextheight, it should be the oldpubkey. - v0, err := LoadValidators(stateDB, nextHeight) + v0, err := sm.LoadValidators(stateDB, nextHeight) assert.Nil(t, err) assert.Equal(t, valSetSize, v0.Size()) index, val := v0.GetByAddress(pubkeyOld.Address()) @@ -867,7 +865,7 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { } // Load nextheight+1, it should be the new pubkey. - v1, err := LoadValidators(stateDB, nextHeight+1) + v1, err := sm.LoadValidators(stateDB, nextHeight+1) assert.Nil(t, err) assert.Equal(t, valSetSize, v1.Size()) index, val = v1.GetByAddress(pubkey.Address()) @@ -877,14 +875,6 @@ func TestManyValidatorChangesSaveLoad(t *testing.T) { } } -func genValSet(size int) *types.ValidatorSet { - vals := make([]*types.Validator, size) - for i := 0; i < size; i++ { - vals[i] = types.NewValidator(ed25519.GenPrivKey().PubKey(), 10) - } - return types.NewValidatorSet(vals) -} - func TestStateMakeBlock(t *testing.T) { tearDown, _, state := setupTestCase(t) defer tearDown(t) @@ -930,14 +920,14 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { changeIndex++ cp = params[changeIndex] } - header, blockID, responses := makeHeaderPartsResponsesParams(state, i, cp) + header, blockID, responses := makeHeaderPartsResponsesParams(state, cp) validatorUpdates, err = types.PB2TM.ValidatorUpdates(responses.EndBlock.ValidatorUpdates) require.NoError(t, err) - state, err = updateState(state, blockID, &header, responses, validatorUpdates) + state, err = sm.UpdateState(state, blockID, &header, responses, validatorUpdates) require.Nil(t, err) nextHeight := state.LastBlockHeight + 1 - saveConsensusParamsInfo(stateDB, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams) + sm.SaveConsensusParamsInfo(stateDB, nextHeight, state.LastHeightConsensusParamsChanged, state.ConsensusParams) } // Make all the test cases by using the same params until after the change. @@ -955,32 +945,15 @@ func TestConsensusParamsChangesSaveLoad(t *testing.T) { } for _, testCase := range testCases { - p, err := LoadConsensusParams(stateDB, testCase.height) + p, err := sm.LoadConsensusParams(stateDB, testCase.height) assert.Nil(t, err, fmt.Sprintf("expected no err at height %d", testCase.height)) assert.Equal(t, testCase.params, p, fmt.Sprintf(`unexpected consensus params at height %d`, testCase.height)) } } -func makeParams( - blockBytes, blockGas int64, - blockTimeIotaMs int64, - evidenceAge int64, -) types.ConsensusParams { - return types.ConsensusParams{ - Block: types.BlockParams{ - MaxBytes: blockBytes, - MaxGas: blockGas, - TimeIotaMs: blockTimeIotaMs, - }, - Evidence: types.EvidenceParams{ - MaxAge: evidenceAge, - }, - } -} - func TestApplyUpdates(t *testing.T) { - initParams := makeParams(1, 2, 3, 4) + initParams := makeConsensusParams(1, 2, 3, 4) cases := [...]struct { init types.ConsensusParams @@ -996,14 +969,14 @@ func TestApplyUpdates(t *testing.T) { MaxGas: 55, }, }, - makeParams(44, 55, 3, 4)}, + makeConsensusParams(44, 55, 3, 4)}, 3: {initParams, abci.ConsensusParams{ Evidence: &abci.EvidenceParams{ MaxAge: 66, }, }, - makeParams(1, 2, 3, 66)}, + makeConsensusParams(1, 2, 3, 66)}, } for i, tc := range cases { @@ -1011,61 +984,3 @@ func TestApplyUpdates(t *testing.T) { assert.Equal(t, tc.expected, res, "case %d", i) } } - -func makeHeaderPartsResponsesValPubKeyChange(state State, height int64, - pubkey crypto.PubKey) (types.Header, types.BlockID, *ABCIResponses) { - - block := makeBlock(state, state.LastBlockHeight+1) - abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, - } - - // If the pubkey is new, remove the old and add the new. - _, val := state.NextValidators.GetByIndex(0) - if !bytes.Equal(pubkey.Bytes(), val.PubKey.Bytes()) { - abciResponses.EndBlock = &abci.ResponseEndBlock{ - ValidatorUpdates: []abci.ValidatorUpdate{ - types.TM2PB.NewValidatorUpdate(val.PubKey, 0), - types.TM2PB.NewValidatorUpdate(pubkey, 10), - }, - } - } - - return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses -} - -func makeHeaderPartsResponsesValPowerChange(state State, height int64, - power int64) (types.Header, types.BlockID, *ABCIResponses) { - - block := makeBlock(state, state.LastBlockHeight+1) - abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ValidatorUpdates: nil}, - } - - // If the pubkey is new, remove the old and add the new. - _, val := state.NextValidators.GetByIndex(0) - if val.VotingPower != power { - abciResponses.EndBlock = &abci.ResponseEndBlock{ - ValidatorUpdates: []abci.ValidatorUpdate{ - types.TM2PB.NewValidatorUpdate(val.PubKey, power), - }, - } - } - - return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses -} - -func makeHeaderPartsResponsesParams(state State, height int64, - params types.ConsensusParams) (types.Header, types.BlockID, *ABCIResponses) { - - block := makeBlock(state, state.LastBlockHeight+1) - abciResponses := &ABCIResponses{ - EndBlock: &abci.ResponseEndBlock{ConsensusParamUpdates: types.TM2PB.ConsensusParams(¶ms)}, - } - return block.Header, types.BlockID{Hash: block.Hash(), PartsHeader: types.PartSetHeader{}}, abciResponses -} - -type paramsChangeTestCase struct { - height int64 - params types.ConsensusParams -} diff --git a/state/store_test.go b/state/store_test.go index 06adeefab..0cf217722 100644 --- a/state/store_test.go +++ b/state/store_test.go @@ -1,4 +1,4 @@ -package state +package state_test import ( "fmt" @@ -10,6 +10,7 @@ import ( cfg "github.com/tendermint/tendermint/config" dbm "github.com/tendermint/tendermint/libs/db" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -19,9 +20,9 @@ func TestStoreLoadValidators(t *testing.T) { vals := types.NewValidatorSet([]*types.Validator{val}) // 1) LoadValidators loads validators using a height where they were last changed - saveValidatorsInfo(stateDB, 1, 1, vals) - saveValidatorsInfo(stateDB, 2, 1, vals) - loadedVals, err := LoadValidators(stateDB, 2) + sm.SaveValidatorsInfo(stateDB, 1, 1, vals) + sm.SaveValidatorsInfo(stateDB, 2, 1, vals) + loadedVals, err := sm.LoadValidators(stateDB, 2) require.NoError(t, err) assert.NotZero(t, loadedVals.Size()) @@ -30,13 +31,13 @@ func TestStoreLoadValidators(t *testing.T) { // TODO(melekes): REMOVE in 0.33 release // https://github.com/tendermint/tendermint/issues/3543 // for releases prior to v0.31.4, it uses last height changed - valInfo := &ValidatorsInfo{ - LastHeightChanged: valSetCheckpointInterval, + valInfo := &sm.ValidatorsInfo{ + LastHeightChanged: sm.ValSetCheckpointInterval, } - stateDB.Set(calcValidatorsKey(valSetCheckpointInterval), valInfo.Bytes()) + stateDB.Set(sm.CalcValidatorsKey(sm.ValSetCheckpointInterval), valInfo.Bytes()) assert.NotPanics(t, func() { - saveValidatorsInfo(stateDB, valSetCheckpointInterval+1, 1, vals) - loadedVals, err := LoadValidators(stateDB, valSetCheckpointInterval+1) + sm.SaveValidatorsInfo(stateDB, sm.ValSetCheckpointInterval+1, 1, vals) + loadedVals, err := sm.LoadValidators(stateDB, sm.ValSetCheckpointInterval+1) if err != nil { t.Fatal(err) } @@ -46,9 +47,9 @@ func TestStoreLoadValidators(t *testing.T) { }) // ENDREMOVE - saveValidatorsInfo(stateDB, valSetCheckpointInterval, 1, vals) + sm.SaveValidatorsInfo(stateDB, sm.ValSetCheckpointInterval, 1, vals) - loadedVals, err = LoadValidators(stateDB, valSetCheckpointInterval) + loadedVals, err = sm.LoadValidators(stateDB, sm.ValSetCheckpointInterval) require.NoError(t, err) assert.NotZero(t, loadedVals.Size()) } @@ -60,20 +61,20 @@ func BenchmarkLoadValidators(b *testing.B) { defer os.RemoveAll(config.RootDir) dbType := dbm.DBBackendType(config.DBBackend) stateDB := dbm.NewDB("state", dbType, config.DBDir()) - state, err := LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) + state, err := sm.LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) if err != nil { b.Fatal(err) } state.Validators = genValSet(valSetSize) state.NextValidators = state.Validators.CopyIncrementProposerPriority(1) - SaveState(stateDB, state) + sm.SaveState(stateDB, state) for i := 10; i < 10000000000; i *= 10 { // 10, 100, 1000, ... - saveValidatorsInfo(stateDB, int64(i), state.LastHeightValidatorsChanged, state.NextValidators) + sm.SaveValidatorsInfo(stateDB, int64(i), state.LastHeightValidatorsChanged, state.NextValidators) b.Run(fmt.Sprintf("height=%d", i), func(b *testing.B) { for n := 0; n < b.N; n++ { - _, err := LoadValidators(stateDB, int64(i)) + _, err := sm.LoadValidators(stateDB, int64(i)) if err != nil { b.Fatal(err) } diff --git a/state/tx_filter_test.go b/state/tx_filter_test.go index e48ad2c3e..bd3243168 100644 --- a/state/tx_filter_test.go +++ b/state/tx_filter_test.go @@ -1,4 +1,4 @@ -package state +package state_test import ( "os" @@ -7,11 +7,10 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" ) func TestTxFilter(t *testing.T) { @@ -34,10 +33,10 @@ func TestTxFilter(t *testing.T) { for i, tc := range testCases { stateDB := dbm.NewDB("state", "memdb", os.TempDir()) - state, err := LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) require.NoError(t, err) - f := TxPreCheck(state) + f := sm.TxPreCheck(state) if tc.isErr { assert.NotNil(t, f(tc.tx), "#%v", i) } else { @@ -45,13 +44,3 @@ func TestTxFilter(t *testing.T) { } } } - -func randomGenesisDoc() *types.GenesisDoc { - pubkey := ed25519.GenPrivKey().PubKey() - return &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: "abc", - Validators: []types.GenesisValidator{{Address: pubkey.Address(), PubKey: pubkey, Power: 10, Name: "myval"}}, - ConsensusParams: types.DefaultConsensusParams(), - } -} diff --git a/state/validation.go b/state/validation.go index 3c63c35b7..1d365e90c 100644 --- a/state/validation.go +++ b/state/validation.go @@ -94,10 +94,7 @@ func validateBlock(evidencePool EvidencePool, stateDB dbm.DB, state State, block } } else { if len(block.LastCommit.Precommits) != state.LastValidators.Size() { - return fmt.Errorf("Invalid block commit size. Expected %v, got %v", - state.LastValidators.Size(), - len(block.LastCommit.Precommits), - ) + return types.NewErrInvalidCommitPrecommits(state.LastValidators.Size(), len(block.LastCommit.Precommits)) } err := state.LastValidators.VerifyCommit( state.ChainID, state.LastBlockID, block.Height-1, block.LastCommit) diff --git a/state/validation_test.go b/state/validation_test.go index 705f843df..c53cf0102 100644 --- a/state/validation_test.go +++ b/state/validation_test.go @@ -1,31 +1,30 @@ -package state +package state_test import ( "testing" "time" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/crypto/tmhash" "github.com/tendermint/tendermint/libs/log" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" ) -// TODO(#2589): -// - generalize this past the first height -// - add txs and build up full State properly -// - test block.Time (see #2587 - there are no conditions on time for the first height) -func TestValidateBlockHeader(t *testing.T) { - var height int64 = 1 // TODO(#2589): generalize - state, stateDB := state(1, int(height)) +const validationTestsStopHeight int64 = 10 - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, nil) +func TestValidateBlockHeader(t *testing.T) { + proxyApp := newTestApp() + require.NoError(t, proxyApp.Start()) + defer proxyApp.Stop() - // A good block passes. - block := makeBlock(state, height) - err := blockExec.ValidateBlock(state, block) - require.NoError(t, err) + state, stateDB, privVals := makeState(3, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) + lastCommit := types.NewCommit(types.BlockID{}, nil) // some bad values wrongHash := tmhash.Sum([]byte("this hash is wrong")) @@ -43,7 +42,7 @@ func TestValidateBlockHeader(t *testing.T) { {"Version wrong2", func(block *types.Block) { block.Version = wrongVersion2 }}, {"ChainID wrong", func(block *types.Block) { block.ChainID = "not-the-real-one" }}, {"Height wrong", func(block *types.Block) { block.Height += 10 }}, - {"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 3600 * 24) }}, + {"Time wrong", func(block *types.Block) { block.Time = block.Time.Add(-time.Second * 1) }}, {"NumTxs wrong", func(block *types.Block) { block.NumTxs += 10 }}, {"TotalTxs wrong", func(block *types.Block) { block.TotalTxs += 10 }}, @@ -62,78 +61,145 @@ func TestValidateBlockHeader(t *testing.T) { {"Proposer invalid", func(block *types.Block) { block.ProposerAddress = []byte("wrong size") }}, } - for _, tc := range testCases { - block := makeBlock(state, height) - tc.malleateBlock(block) - err := blockExec.ValidateBlock(state, block) - require.Error(t, err, tc.name) + // Build up state for multiple heights + for height := int64(1); height < validationTestsStopHeight; height++ { + proposerAddr := state.Validators.GetProposer().Address + /* + Invalid blocks don't pass + */ + for _, tc := range testCases { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, proposerAddr) + tc.malleateBlock(block) + err := blockExec.ValidateBlock(state, block) + require.Error(t, err, tc.name) + } + + /* + A good block passes + */ + var err error + state, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil) + require.NoError(t, err, "height %d", height) } } -/* - TODO(#2589): - - test Block.Data.Hash() == Block.DataHash - - test len(Block.Data.Txs) == Block.NumTxs -*/ -func TestValidateBlockData(t *testing.T) { -} - -/* - TODO(#2589): - - test len(block.LastCommit.Precommits) == state.LastValidators.Size() - - test state.LastValidators.VerifyCommit -*/ func TestValidateBlockCommit(t *testing.T) { + proxyApp := newTestApp() + require.NoError(t, proxyApp.Start()) + defer proxyApp.Stop() + + state, stateDB, privVals := makeState(1, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) + lastCommit := types.NewCommit(types.BlockID{}, nil) + wrongPrecommitsCommit := types.NewCommit(types.BlockID{}, nil) + badPrivVal := types.NewMockPV() + + for height := int64(1); height < validationTestsStopHeight; height++ { + proposerAddr := state.Validators.GetProposer().Address + if height > 1 { + /* + #2589: ensure state.LastValidators.VerifyCommit fails here + */ + // should be height-1 instead of height + wrongHeightVote, err := makeVote(height, state.LastBlockID, state.Validators, privVals[proposerAddr.String()]) + require.NoError(t, err, "height %d", height) + wrongHeightCommit := types.NewCommit(state.LastBlockID, []*types.CommitSig{wrongHeightVote.CommitSig()}) + block, _ := state.MakeBlock(height, makeTxs(height), wrongHeightCommit, nil, proposerAddr) + err = blockExec.ValidateBlock(state, block) + _, isErrInvalidCommitHeight := err.(types.ErrInvalidCommitHeight) + require.True(t, isErrInvalidCommitHeight, "expected ErrInvalidCommitHeight at height %d but got: %v", height, err) + + /* + #2589: test len(block.LastCommit.Precommits) == state.LastValidators.Size() + */ + block, _ = state.MakeBlock(height, makeTxs(height), wrongPrecommitsCommit, nil, proposerAddr) + err = blockExec.ValidateBlock(state, block) + _, isErrInvalidCommitPrecommits := err.(types.ErrInvalidCommitPrecommits) + require.True(t, isErrInvalidCommitPrecommits, "expected ErrInvalidCommitPrecommits at height %d but got: %v", height, err) + } + + /* + A good block passes + */ + var err error + var blockID types.BlockID + state, blockID, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, nil) + require.NoError(t, err, "height %d", height) + + /* + wrongPrecommitsCommit is fine except for the extra bad precommit + */ + goodVote, err := makeVote(height, blockID, state.Validators, privVals[proposerAddr.String()]) + require.NoError(t, err, "height %d", height) + badVote := &types.Vote{ + ValidatorAddress: badPrivVal.GetPubKey().Address(), + ValidatorIndex: 0, + Height: height, + Round: 0, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + err = badPrivVal.SignVote(chainID, goodVote) + require.NoError(t, err, "height %d", height) + wrongPrecommitsCommit = types.NewCommit(blockID, []*types.CommitSig{goodVote.CommitSig(), badVote.CommitSig()}) + } } -/* - TODO(#2589): - - test good/bad evidence in block -*/ func TestValidateBlockEvidence(t *testing.T) { - var height int64 = 1 // TODO(#2589): generalize - state, stateDB := state(1, int(height)) - - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, nil) - - // make some evidence - addr, _ := state.Validators.GetByIndex(0) - goodEvidence := types.NewMockGoodEvidence(height, 0, addr) - - // A block with a couple pieces of evidence passes. - block := makeBlock(state, height) - block.Evidence.Evidence = []types.Evidence{goodEvidence, goodEvidence} - block.EvidenceHash = block.Evidence.Hash() - err := blockExec.ValidateBlock(state, block) - require.NoError(t, err) - - // A block with too much evidence fails. - maxBlockSize := state.ConsensusParams.Block.MaxBytes - maxNumEvidence, _ := types.MaxEvidencePerBlock(maxBlockSize) - require.True(t, maxNumEvidence > 2) - for i := int64(0); i < maxNumEvidence; i++ { - block.Evidence.Evidence = append(block.Evidence.Evidence, goodEvidence) + proxyApp := newTestApp() + require.NoError(t, proxyApp.Start()) + defer proxyApp.Stop() + + state, stateDB, privVals := makeState(3, 1) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), proxyApp.Consensus(), mock.Mempool{}, sm.MockEvidencePool{}) + lastCommit := types.NewCommit(types.BlockID{}, nil) + + for height := int64(1); height < validationTestsStopHeight; height++ { + proposerAddr := state.Validators.GetProposer().Address + proposerIdx, _ := state.Validators.GetByAddress(proposerAddr) + goodEvidence := types.NewMockGoodEvidence(height, proposerIdx, proposerAddr) + if height > 1 { + /* + A block with too much evidence fails + */ + maxBlockSize := state.ConsensusParams.Block.MaxBytes + maxNumEvidence, _ := types.MaxEvidencePerBlock(maxBlockSize) + require.True(t, maxNumEvidence > 2) + evidence := make([]types.Evidence, 0) + // one more than the maximum allowed evidence + for i := int64(0); i <= maxNumEvidence; i++ { + evidence = append(evidence, goodEvidence) + } + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, evidence, proposerAddr) + err := blockExec.ValidateBlock(state, block) + _, ok := err.(*types.ErrEvidenceOverflow) + require.True(t, ok, "expected error to be of type ErrEvidenceOverflow at height %d", height) + } + + /* + A good block with several pieces of good evidence passes + */ + maxBlockSize := state.ConsensusParams.Block.MaxBytes + maxNumEvidence, _ := types.MaxEvidencePerBlock(maxBlockSize) + require.True(t, maxNumEvidence > 2) + evidence := make([]types.Evidence, 0) + // precisely the amount of allowed evidence + for i := int64(0); i < maxNumEvidence; i++ { + evidence = append(evidence, goodEvidence) + } + + var err error + state, _, lastCommit, err = makeAndCommitGoodBlock(state, height, lastCommit, proposerAddr, blockExec, privVals, evidence) + require.NoError(t, err, "height %d", height) } - block.EvidenceHash = block.Evidence.Hash() - err = blockExec.ValidateBlock(state, block) - require.Error(t, err) - _, ok := err.(*types.ErrEvidenceOverflow) - require.True(t, ok) } -// always returns true if asked if any evidence was already committed. -type mockEvPoolAlwaysCommitted struct{} - -func (m mockEvPoolAlwaysCommitted) PendingEvidence(int64) []types.Evidence { return nil } -func (m mockEvPoolAlwaysCommitted) AddEvidence(types.Evidence) error { return nil } -func (m mockEvPoolAlwaysCommitted) Update(*types.Block, State) {} -func (m mockEvPoolAlwaysCommitted) IsCommitted(types.Evidence) bool { return true } - func TestValidateFailBlockOnCommittedEvidence(t *testing.T) { var height int64 = 1 - state, stateDB := state(1, int(height)) + state, stateDB, _ := makeState(1, int(height)) - blockExec := NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, mockEvPoolAlwaysCommitted{}) + blockExec := sm.NewBlockExecutor(stateDB, log.TestingLogger(), nil, nil, mockEvPoolAlwaysCommitted{}) // A block with a couple pieces of evidence passes. block := makeBlock(state, height) addr, _ := state.Validators.GetByIndex(0) @@ -145,12 +211,3 @@ func TestValidateFailBlockOnCommittedEvidence(t *testing.T) { require.Error(t, err) require.IsType(t, err, &types.ErrEvidenceInvalid{}) } - -/* - TODO(#2589): - - test unmarshalling BlockParts that are too big into a Block that - (note this logic happens in the consensus, not in the validation here). - - test making blocks from the types.MaxXXX functions works/fails as expected -*/ -func TestValidateBlockSize(t *testing.T) { -} diff --git a/types/errors.go b/types/errors.go new file mode 100644 index 000000000..603ac51d7 --- /dev/null +++ b/types/errors.go @@ -0,0 +1,41 @@ +package types + +import "fmt" + +type ( + // ErrInvalidCommitHeight is returned when we encounter a commit with an + // unexpected height. + ErrInvalidCommitHeight struct { + Expected int64 + Actual int64 + } + + // ErrInvalidCommitPrecommits is returned when we encounter a commit where + // the number of precommits doesn't match the number of validators. + ErrInvalidCommitPrecommits struct { + Expected int + Actual int + } +) + +func NewErrInvalidCommitHeight(expected, actual int64) ErrInvalidCommitHeight { + return ErrInvalidCommitHeight{ + Expected: expected, + Actual: actual, + } +} + +func (e ErrInvalidCommitHeight) Error() string { + return fmt.Sprintf("Invalid commit -- wrong height: %v vs %v", e.Expected, e.Actual) +} + +func NewErrInvalidCommitPrecommits(expected, actual int) ErrInvalidCommitPrecommits { + return ErrInvalidCommitPrecommits{ + Expected: expected, + Actual: actual, + } +} + +func (e ErrInvalidCommitPrecommits) Error() string { + return fmt.Sprintf("Invalid commit -- wrong set size: %v vs %v", e.Expected, e.Actual) +} diff --git a/types/validator_set.go b/types/validator_set.go index 9e78fbc77..65358714d 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -594,10 +594,10 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i return err } if vals.Size() != len(commit.Precommits) { - return fmt.Errorf("Invalid commit -- wrong set size: %v vs %v", vals.Size(), len(commit.Precommits)) + return NewErrInvalidCommitPrecommits(vals.Size(), len(commit.Precommits)) } if height != commit.Height() { - return fmt.Errorf("Invalid commit -- wrong height: %v vs %v", height, commit.Height()) + return NewErrInvalidCommitHeight(height, commit.Height()) } if !blockID.Equals(commit.BlockID) { return fmt.Errorf("Invalid commit -- wrong block id: want %v got %v", From 2779f8fab3250e3de202ff66b34188f35f3cacef Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 22 Jun 2019 10:30:23 +0400 Subject: [PATCH 132/211] node: run whole func in goroutine, not just logger.Error fn (#3743) * node: run whole func in goroutine, not just logger.Error fn Fixes #3741 * add a changelog entry --- CHANGELOG_PENDING.md | 1 + node/node.go | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 6fa55c0cb..19b62ec2a 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -37,3 +37,4 @@ - [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) - [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) - [node] \#3716 Fix a bug where `nil` is recorded as node's address +- [node] \#3741 Fix profiler blocking the entire node diff --git a/node/node.go b/node/node.go index 85fef5ee7..c992e2424 100644 --- a/node/node.go +++ b/node/node.go @@ -630,7 +630,9 @@ func NewNode(config *cfg.Config, } if config.ProfListenAddress != "" { - go logger.Error("Profile server", "err", http.ListenAndServe(config.ProfListenAddress, nil)) + go func() { + logger.Error("Profile server", "err", http.ListenAndServe(config.ProfListenAddress, nil)) + }() } node := &Node{ From 99d798f508bab08ebf85076482dc916a02bdf9c8 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 22 Jun 2019 19:44:12 +0400 Subject: [PATCH 133/211] rpc/lib: write a test for TLS server (#3703) * rpc/lib: write a test for TLS server Refs #3700 * do not regenerate certificates * add nolint --- Makefile | 27 ++++++++++------ libs/db/remotedb/test.crt | 40 ++++++++++++++---------- libs/db/remotedb/test.key | 50 +++++++++++++++--------------- libs/test.sh | 6 ---- rpc/lib/server/http_server_test.go | 33 +++++++++++++------- rpc/lib/server/test.crt | 25 +++++++++++++++ rpc/lib/server/test.key | 27 ++++++++++++++++ 7 files changed, 139 insertions(+), 69 deletions(-) create mode 100644 rpc/lib/server/test.crt create mode 100644 rpc/lib/server/test.key diff --git a/Makefile b/Makefile index 1980ac861..3dff35b9c 100644 --- a/Makefile +++ b/Makefile @@ -115,24 +115,31 @@ get_deps_bin_size: protoc_libs: libs/common/types.pb.go +# generates certificates for TLS testing in remotedb and RPC server gen_certs: clean_certs - ## Generating certificates for TLS testing... certstrap init --common-name "tendermint.com" --passphrase "" - certstrap request-cert -ip "::" --passphrase "" - certstrap sign "::" --CA "tendermint.com" --passphrase "" - mv out/::.crt out/::.key db/remotedb + certstrap request-cert --common-name "remotedb" -ip "127.0.0.1" --passphrase "" + certstrap sign "remotedb" --CA "tendermint.com" --passphrase "" + mv out/remotedb.crt libs/db/remotedb/test.crt + mv out/remotedb.key libs/db/remotedb/test.key + certstrap request-cert --common-name "server" -ip "127.0.0.1" --passphrase "" + certstrap sign "server" --CA "tendermint.com" --passphrase "" + mv out/server.crt rpc/lib/server/test.crt + mv out/server.key rpc/lib/server/test.key + rm -rf out +# deletes generated certificates clean_certs: - ## Cleaning TLS testing certificates... - rm -rf out - rm -f db/remotedb/::.crt db/remotedb/::.key + rm -f libs/db/remotedb/test.crt + rm -f libs/db/remotedb/test.key + rm -f rpc/lib/server/test.crt + rm -f rpc/lib/server/test.key -test_libs: gen_certs +test_libs: go test -tags clevedb boltdb $(PACKAGES) - make clean_certs grpc_dbserver: - protoc -I db/remotedb/proto/ db/remotedb/proto/defs.proto --go_out=plugins=grpc:db/remotedb/proto + protoc -I libs/db/remotedb/proto/ libs/db/remotedb/proto/defs.proto --go_out=plugins=grpc:libs/db/remotedb/proto protoc_grpc: rpc/grpc/types.pb.go diff --git a/libs/db/remotedb/test.crt b/libs/db/remotedb/test.crt index 06ffec1d2..1090e73d7 100644 --- a/libs/db/remotedb/test.crt +++ b/libs/db/remotedb/test.crt @@ -1,19 +1,25 @@ -----BEGIN CERTIFICATE----- -MIIDAjCCAeqgAwIBAgIJAOGCVedOwRbOMA0GCSqGSIb3DQEBBQUAMCExCzAJBgNV -BAYTAlVTMRIwEAYDVQQDDAlsb2NhbGhvc3QwHhcNMTkwMjExMTU0NjQ5WhcNMjAw -MjExMTU0NjQ5WjAhMQswCQYDVQQGEwJVUzESMBAGA1UEAwwJbG9jYWxob3N0MIIB -IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA60S/fNUWoHm1PYI/yrlnZNtr -dRqDORHe0hPwl/lttLz7+a7HzQZFnpiXnuxbDJtpIq/h1vhAl0sFy86Ip26LhbWc -GjxJL24tVwiOwqYRzTPZ/rK3JYuNcIvcztXjMqdzPrHSZy5YZgrQB6yhTiqpBc4D -h/XgWjEt4DhpHwf/zuIK9XkJw0IaTWjFmoyKRoWW3q4bHzoKNxS9bXP117Tz7tn0 -AdsQCjt1GKcIROkcOGUHqByINJ2XlBkb7SQPjQVBLDVJKdRDUt+yHkkdbn97UDhq -HRTCt5UELWs/53Gj1ffNuhjECOVjG1HkZweLgZjJRQYe8X2OOLNOyfVY1KsDnQID -AQABoz0wOzAMBgNVHRMEBTADAQH/MCsGA1UdEQQkMCKCCWxvY2FsaG9zdIIJbG9j -YWxob3N0hwQAAAAAhwR/AAABMA0GCSqGSIb3DQEBBQUAA4IBAQCe2A5gDc3jiZwT -a5TJrc2J2KouqxB/PCddw5VY8jPsZJfsr9gxHi+Xa5g8p3oqmEOIlqM5BVhrZRUG -RWHDmL+bCsuzMoA/vGHtHmUIwLeZQLWgT3kv12Dc8M9flNNjmXWxdMR9lOMwcL83 -F0CdElxSmaEbNvCIJBDetJJ7vMCqS2lnTLWurbH4ZGeGwvjzNgpgGCKwbyK/gU+j -UXiTQbVvPQ3WWACDnfH6rg0TpxU9jOBkd+4/9tUrBG7UclQBfGULk3sObLO9kx4N -8RxJmtp8jljIXVPX3udExI05pz039pAgvaeZWtP17QSbYcKF1jFtKo6ckrv2GKXX -M5OXGXdw +MIIEOjCCAiKgAwIBAgIQYO+jRR0Sbs+WzU/hj2aoxzANBgkqhkiG9w0BAQsFADAZ +MRcwFQYDVQQDEw50ZW5kZXJtaW50LmNvbTAeFw0xOTA2MDIxMTAyMDdaFw0yMDEy +MDIxMTAyMDRaMBMxETAPBgNVBAMTCHJlbW90ZWRiMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAt7YkYMJ5X5X3MT1tWG1KFO3uyZl962fInl+43xVESydp +qYYHYei7b3T8c/3Ww6f3aKkkCHrvPtqHZjU6o+wp/AQMNlyUoyRN89+6Oj67u2C7 +iZjzAJ+Pk87jMaStubvmZ9J+uk4op4rv5Rt4ns/Kg70RaMvqYR8tGqPcy3o8fWS+ +hCbuwAS8b65yp+AgbnThDEBUnieN3OFLfDV//45qw2OlqlM/gHOVT2JMRbl14Y7x +tW3/Xe+lsB7B3+OC6NQ2Nu7DEA1X+TBNyItIGnQH6DwK2ZBRtyQEk26FAWVj8fHd +A5I4+RcGWXz4T6gJmDZN7+47WHO0ProjARbUV0GIuQIDAQABo4GDMIGAMA4GA1Ud +DwEB/wQEAwIDuDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwHQYDVR0O +BBYEFOA8wzCYhoZmy0WHgnv/0efijUMKMB8GA1UdIwQYMBaAFNSTPe743aIx7rIp +vn5HV3gJ4z1hMA8GA1UdEQQIMAaHBH8AAAEwDQYJKoZIhvcNAQELBQADggIBAKZf +EVo0i9nMZv6ZJjbmAlMfo5FH41/oBYC8pyGAnJKl42raXKJAbl45h80iGn3vNggf +7HJjN+znAHDFYjIwK2IV2WhHPyxK6uk+FA5uBR/aAPcw+zhRfXUMYdhNHr6KBlZZ +bvD7Iq4UALg+XFQz/fQkIi7QvTBwkYyPNA2+a/TGf6myMp26hoz73DQXklqm6Zle +myPs1Vp9bTgOv/3l64BMUV37FZ2TyiisBkV1qPEoDxT7Fbi8G1K8gMDLd0wu0jvX +nz96nk30TDnZewV1fhkMJVKKGiLbaIgHcu1lWsWJZ0tdc+MF7R9bLBO5T0cTDgNy +V8/51g+Cxu5SSHKjFkT0vBBONhjPmRqzJpxOQfHjiv8mmHwwiaNNy2VkJHj5GHer +64r67fQTSqAifzgwAbXYK+ObUbx4PnHvSYSF5dbcR1Oj6UTVtGAgdmN2Y03AIc1B +CiaojcMVuMRz/SvmPWl34GBvvT5/h9VCpHEB3vV6bQxJb5U1fLyo4GABA2Ic3DHr +kV5p7CZI06UNbyQyFtnEb5XoXywRa4Df7FzDIv3HL13MtyXrYrJqC1eAbn+3jGdh +bQa510mWYAlQQmzHSf/SLKott4QKR3SmhOGqGKNvquAYJ9XLdYdsPmKKGH6iGUD8 +n7yEi0KMD/BHsPQNNLatsR2SxqGDeLhbLR0w2hig -----END CERTIFICATE----- diff --git a/libs/db/remotedb/test.key b/libs/db/remotedb/test.key index e1adb3e1e..b30bf809a 100644 --- a/libs/db/remotedb/test.key +++ b/libs/db/remotedb/test.key @@ -1,27 +1,27 @@ -----BEGIN RSA PRIVATE KEY----- -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 -----END RSA PRIVATE KEY----- diff --git a/libs/test.sh b/libs/test.sh index 64898b0d2..d0618768b 100755 --- a/libs/test.sh +++ b/libs/test.sh @@ -4,9 +4,6 @@ set -e # run the linter # make lint -# setup certs -make gen_certs - # run the unit tests with coverage echo "" > coverage.txt for d in $(go list ./... | grep -v vendor); do @@ -16,6 +13,3 @@ for d in $(go list ./... | grep -v vendor); do rm profile.out fi done - -# cleanup certs -make clean_certs diff --git a/rpc/lib/server/http_server_test.go b/rpc/lib/server/http_server_test.go index 7f47a30b3..b463aa6a8 100644 --- a/rpc/lib/server/http_server_test.go +++ b/rpc/lib/server/http_server_test.go @@ -1,16 +1,18 @@ package rpcserver import ( + "crypto/tls" "fmt" "io" "io/ioutil" + "net" "net/http" - "os" "sync" "sync/atomic" "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tendermint/tendermint/libs/log" @@ -66,18 +68,27 @@ func TestMaxOpenConnections(t *testing.T) { } func TestStartHTTPAndTLSServer(t *testing.T) { - config := DefaultConfig() - config.MaxOpenConnections = 1 - // set up fixtures - listenerAddr := "tcp://0.0.0.0:0" - listener, err := Listen(listenerAddr, config) + ln, err := net.Listen("tcp", "localhost:0") require.NoError(t, err) + defer ln.Close() + mux := http.NewServeMux() - mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {}) + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, "some body") + }) - // test failure - err = StartHTTPAndTLSServer(listener, mux, "", "", log.TestingLogger(), config) - require.IsType(t, (*os.PathError)(nil), err) + go StartHTTPAndTLSServer(ln, mux, "test.crt", "test.key", log.TestingLogger(), DefaultConfig()) - // TODO: test that starting the server can actually work + tr := &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, // nolint: gosec + } + c := &http.Client{Transport: tr} + res, err := c.Get("https://" + ln.Addr().String()) + require.NoError(t, err) + defer res.Body.Close() + assert.Equal(t, http.StatusOK, res.StatusCode) + + body, err := ioutil.ReadAll(res.Body) + require.NoError(t, err) + assert.Equal(t, []byte("some body"), body) } diff --git a/rpc/lib/server/test.crt b/rpc/lib/server/test.crt new file mode 100644 index 000000000..e4ab1965d --- /dev/null +++ b/rpc/lib/server/test.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIEODCCAiCgAwIBAgIQWDHUrd4tOM2xExWhzOEJ7DANBgkqhkiG9w0BAQsFADAZ +MRcwFQYDVQQDEw50ZW5kZXJtaW50LmNvbTAeFw0xOTA2MDIxMTAyMDdaFw0yMDEy +MDIxMTAyMDRaMBExDzANBgNVBAMTBnNlcnZlcjCCASIwDQYJKoZIhvcNAQEBBQAD +ggEPADCCAQoCggEBANBaa6dc9GZcIhAHWqVrx0LONYf+IlbvTP7yrV45ws0ix8TX +1NUOiDY1cwzKH8ay/HYX45e2fFLrtLidc9h+apsC55k3Vdcy00+Ksr/adjR8D4A/ +GpnTS+hVDHTlqINe9a7USok34Zr1rc3fh4Imu5RxEurjMwkA/36k6+OpXMp2qlKY +S1fGqwn2KGhXkp/yTWZILEMXBazNxGx4xfqYXzWm6boeyJAXpM2DNkv7dtwa/CWY +WacUQJApNInwn5+B8LLoo+pappkfZOjAD9/aHKsyFTSWmmWeg7V//ouB3u5vItqf +GP+3xmPgeYeEyOIe/P2f8bRuQs+GGwSCmi6F1GUCAwEAAaOBgzCBgDAOBgNVHQ8B +Af8EBAMCA7gwHQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMB0GA1UdDgQW +BBSpBFIMbkBR4xVYQZtUJQQwzPmbHjAfBgNVHSMEGDAWgBTUkz3u+N2iMe6yKb5+ +R1d4CeM9YTAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4ICAQBCqdzS +tPHkMYWjYs6aREwob9whjyG8a4Qp6IkP1SYHCwpzsTeWLi9ybEcDRb3jZ4iRxbZg +7GFxjqHoWgBZHAIyICMsHupOJEtXq5hx86NuMwk/12bx1eNj0yTIAnVOA+em/ZtB +zR38OwB8xXmjKd0Ow1Y7zCh5zE2gU+sR0JOJSfxXUZrJvwDNrbcmZPQ+kwuq4cyv +fxZnvZf/owbyOLQFdbiPQbbiZ7JSv8q7GCMleULCEygrsWClYkULUByhKykCHJIU +wfq1owge9EqG/4CDCCjB9vBFmUyv3FJhgWnzd6tPQckFoHSoD0Bjsv/pQFcsGLcg ++e/Mm6hZgCXXwI2WHYbxqz5ToOaRQQYo6N77jWejOBMecOZmPDyQ2nz73aJd11GW +NiDT7pyMlBJA8W4wAvVP4ow2ugqsPjqZ6EyismIGFUTqMp+NtXOsLPK+sEMhKhJ9 +ulczRpPEf25roBt6aEk2fTAfAPmbpvNamBLSbBU23mzJ38RmfhxLOlOgCGbBBX4d +kE+/+En8UJO4X8CKaKRo/c5G2UZ6++2cjp6SPrsGENDMW5yBGegrDw+ow8/bLxIr +OjWpSe2cygovy3aHE6UBOgkxw9KIaSEqFgjQZ0i+xO6l6qQoljQgUGXfecVMR+7C +4KsyVVTMlK9/thA7Zfc8a5z8ZCtIKkT52XsJhw== +-----END CERTIFICATE----- diff --git a/rpc/lib/server/test.key b/rpc/lib/server/test.key new file mode 100644 index 000000000..bb9af06b0 --- /dev/null +++ b/rpc/lib/server/test.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEoQIBAAKCAQEA0Fprp1z0ZlwiEAdapWvHQs41h/4iVu9M/vKtXjnCzSLHxNfU +1Q6INjVzDMofxrL8dhfjl7Z8Uuu0uJ1z2H5qmwLnmTdV1zLTT4qyv9p2NHwPgD8a +mdNL6FUMdOWog171rtRKiTfhmvWtzd+Hgia7lHES6uMzCQD/fqTr46lcynaqUphL +V8arCfYoaFeSn/JNZkgsQxcFrM3EbHjF+phfNabpuh7IkBekzYM2S/t23Br8JZhZ +pxRAkCk0ifCfn4Hwsuij6lqmmR9k6MAP39ocqzIVNJaaZZ6DtX/+i4He7m8i2p8Y +/7fGY+B5h4TI4h78/Z/xtG5Cz4YbBIKaLoXUZQIDAQABAoH/NodzpVmunRt/zrIe +By0t+U3+tJjOY/I9NHxO41o6oXV40wupqBkljQpwEejUaCxv5nhaGFqqLwmBQs/y +gbaUL/2Sn4bb8HZc13R1U8DZLuNJK0dYrumd9DBOEkoI0FkJ87ebyk3VvbiOxFK8 +JFP+w9rUGKVdtf2M4JhJJEwu/M2Yawx9/8CrCIY2G6ufaylrIysLeQMsxrogF8n4 +hq7fyqveWRzxhqUxS2fp9Ynpx4jnd1lMzv+z3i8eEsW+gB9yke7UkXZMbtZg1xfB +JjiEfcDVfSwSihhgOYttgQ9hkIdohDUak7OzRSWVBuoxWUhMfrQxw/HZlgZJL9Vf +rGdlAoGBANOGmgEGky+acV33WTWGV5OdAw6B/SlBEoORJbj6UzQiUz3hFH/Tgpbj +JOKHWGbGd8OtOYbt9JoofGlNgHA/4nAEYAc2HGa+q0fBwMUflU0DudAxXis4jDmE +D76moGmyJoSgwVrp1W/vwNixA5RpcZ3Wst2nf9RKLr+DxypHTit/AoGBAPwpDeqc +rwXOTl0KR/080Nc11Z03VIVZAGfA59J73HmADF9bBVlmReQdkwX0lERchdzD0lfa +XqbqBLr4FS5Uqyn5f3DCaMnOeKfvtGw2z6LnY+w03mii4PEW/vNKLlB18NdduPwL +KeAc08Zh+qJFMKD1PoEQOH+Y7NybBbaQL8IbAoGAfPPUYaq6o7I+Kd4FysKTVVW5 +CobrP8V65FGH0R++qttkBPfDHkeZqvx/O3nsVLoE4YigpP5IMhCcfbAUoTp7zuQm +vdvPJzqW/4qLD2c60QXUbBHdqPZ8jzVd/6d6tzVP36T+02+yb69XYiofDTrErRK5 +EorxzjwMJYH40xbQLI0CgYBh7d/FucwPSSwN3ixPIQtKSVIImLBuiT4rDTP6/reF +SEGF1ueg7KNAEGxE59OdKQGj1zkdfWU9Fa14n1g6gg9nYcoolJf1qAYb0nAThsXk +0lBwL6ggowERIIkrGygZf3Rlb7SjzgIZU5i7dtnLo2tbV2NK5G3MwCtdEaeKWzzw ++QKBgQC7+JPHoqbnNgis2vCGLKMOU3HpJK/rYEU/8ZUegc9lshEFZYsRbtKQQJQs +nqsChrG8UoK84frujEBkO/Nzsil85p8ar79wZguGnVvswTWaTuKvl8H/qQQ/JSHZ +OHGQD4qwTCkdRr8Vf8NfuCoZlJDnHncLJZNWjrb5feqCnJ/YIQ== +-----END RSA PRIVATE KEY----- From 11cc8d1d9b6fe32d0d68b793948c0cdbaa340516 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 22 Jun 2019 19:48:01 +0400 Subject: [PATCH 134/211] cs: exit if SwitchToConsensus fails (#3706) Refs #3656 --- CHANGELOG_PENDING.md | 1 + consensus/reactor.go | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 19b62ec2a..0b92e8ce0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -29,6 +29,7 @@ ### FEATURES: ### IMPROVEMENTS: +- [consensus] \#3656 Exit if SwitchToConsensus fails - [p2p] \#3666 Add per channel telemetry to improve reactor observability - [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) - [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) diff --git a/consensus/reactor.go b/consensus/reactor.go index f690a407d..dc3514b21 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -116,8 +116,13 @@ func (conR *ConsensusReactor) SwitchToConsensus(state sm.State, blocksSynced int } err := conR.conS.Start() if err != nil { - conR.Logger.Error("Error starting conS", "err", err) - return + panic(fmt.Sprintf(`Failed to start consensus state: %v + +conS: +%+v + +conR: +%+v`, err, conR.conS, conR)) } } From 15443990012a528ddc614e6e09934ea3fe66bdb0 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 24 Jun 2019 15:48:21 +0200 Subject: [PATCH 135/211] Remove double lint (#3748) - Circel CI and Golangci were doing linting jobs, removed circleci Signed-off-by: Marko Baricevic --- .circleci/config.yml | 65 ++++++++++++++++---------------------------- 1 file changed, 23 insertions(+), 42 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 04a03eb25..5836b4546 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -67,22 +67,6 @@ jobs: export PATH="$GOBIN:$PATH" make build-slate - lint: - <<: *defaults - steps: - - attach_workspace: - at: /tmp/workspace - - restore_cache: - key: v4-pkg-cache - - restore_cache: - key: v3-tree-{{ .Environment.CIRCLE_SHA1 }} - - run: - name: metalinter - command: | - set -ex - export PATH="$GOBIN:$PATH" - make lint - test_abci_apps: <<: *defaults steps: @@ -98,8 +82,8 @@ jobs: export PATH="$GOBIN:$PATH" bash abci/tests/test_app/test.sh -# if this test fails, fix it and update the docs at: -# https://github.com/tendermint/tendermint/blob/develop/docs/abci-cli.md + # if this test fails, fix it and update the docs at: + # https://github.com/tendermint/tendermint/blob/develop/docs/abci-cli.md test_abci_cli: <<: *defaults steps: @@ -169,24 +153,24 @@ jobs: command: bash test/persist/test_failure_indices.sh localnet: - working_directory: /home/circleci/.go_workspace/src/github.com/tendermint/tendermint - machine: - image: circleci/classic:latest - environment: - GOBIN: /home/circleci/.go_workspace/bin - GOPATH: /home/circleci/.go_workspace/ - GOOS: linux - GOARCH: amd64 - parallelism: 1 - steps: - - checkout - - run: - name: run localnet and exit on failure - command: | - set -x - docker run --rm -v "$PWD":/go/src/github.com/tendermint/tendermint -w /go/src/github.com/tendermint/tendermint golang make build-linux - make localnet-start & - ./scripts/localnet-blocks-test.sh 40 5 10 localhost + working_directory: /home/circleci/.go_workspace/src/github.com/tendermint/tendermint + machine: + image: circleci/classic:latest + environment: + GOBIN: /home/circleci/.go_workspace/bin + GOPATH: /home/circleci/.go_workspace/ + GOOS: linux + GOARCH: amd64 + parallelism: 1 + steps: + - checkout + - run: + name: run localnet and exit on failure + command: | + set -x + docker run --rm -v "$PWD":/go/src/github.com/tendermint/tendermint -w /go/src/github.com/tendermint/tendermint golang make build-linux + make localnet-start & + ./scripts/localnet-blocks-test.sh 40 5 10 localhost test_p2p: environment: @@ -273,9 +257,9 @@ jobs: paths: - "release-version.source" - save_cache: - key: v2-release-deps-{{ checksum "go.sum" }} - paths: - - "/go/pkg/mod" + key: v2-release-deps-{{ checksum "go.sum" }} + paths: + - "/go/pkg/mod" build_artifacts: <<: *defaults @@ -358,9 +342,6 @@ workflows: - master - develop - setup_dependencies - - lint: - requires: - - setup_dependencies - test_abci_apps: requires: - setup_dependencies From 7af502b4cb5ddb98d9a14cb07e317843829ed6d6 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 24 Jun 2019 17:49:49 +0400 Subject: [PATCH 136/211] docs: (rpc/broadcast_tx_*) write expectations for a client (#3749) Refs #3322 --- rpc/core/mempool.go | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/rpc/core/mempool.go b/rpc/core/mempool.go index 967466e73..1a0954438 100644 --- a/rpc/core/mempool.go +++ b/rpc/core/mempool.go @@ -19,6 +19,19 @@ import ( // Returns right away, with no response. Does not wait for CheckTx nor // DeliverTx results. // +// If you want to be sure that the transaction is included in a block, you can +// subscribe for the result using JSONRPC via a websocket. See +// https://tendermint.com/docs/app-dev/subscribing-to-events-via-websocket.html +// If you haven't received anything after a couple of blocks, resend it. If the +// same happens again, send it to some other node. A few reasons why it could +// happen: +// +// 1. malicious node can drop or pretend it had committed your tx +// 2. malicious proposer (not necessary the one you're communicating with) can +// drop transactions, which might become valid in the future +// (https://github.com/tendermint/tendermint/issues/3322) +// 3. node can be offline +// // Please refer to // https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting // for formatting/encoding rules. @@ -69,6 +82,18 @@ func BroadcastTxAsync(ctx *rpctypes.Context, tx types.Tx) (*ctypes.ResultBroadca // Returns with the response from CheckTx. Does not wait for DeliverTx result. // +// If you want to be sure that the transaction is included in a block, you can +// subscribe for the result using JSONRPC via a websocket. See +// https://tendermint.com/docs/app-dev/subscribing-to-events-via-websocket.html +// If you haven't received anything after a couple of blocks, resend it. If the +// same happens again, send it to some other node. A few reasons why it could +// happen: +// +// 1. malicious node can drop or pretend it had committed your tx +// 2. malicious proposer (not necessary the one you're communicating with) can +// drop transactions, which might become valid in the future +// (https://github.com/tendermint/tendermint/issues/3322) +// // Please refer to // https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting // for formatting/encoding rules. From f64a398e79ed6969b6242ee2632106d69e593aec Mon Sep 17 00:00:00 2001 From: Thane Thomson Date: Mon, 24 Jun 2019 10:32:12 -0400 Subject: [PATCH 137/211] Make RPC bind to localhost by default (#3746) * Make RPC bind to localhost by default * Add CHANGELOG_PENDING entry * Allow testnet command to override RPC listen address * Update localnet test to bind RPC to 0.0.0.0 * Update p2p test to bind RPC to 0.0.0.0 * Remove rpc-laddr parameter * Update localnet to use config template with RPC listen address override * Use config template override method for RPC listen address * Build config template into localnode image * Build localnode image locally before starting localnet * Move testnet config overrides into templates * Revert deletion of config overrides * Remove extraneous config parameter overrides --- CHANGELOG_PENDING.md | 1 + Makefile | 4 ++-- config/config.go | 2 +- networks/local/localnode/Dockerfile | 2 +- networks/local/localnode/config-template.toml | 2 ++ test/docker/Dockerfile | 7 ++++++- test/docker/config-template.toml | 2 ++ 7 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 networks/local/localnode/config-template.toml create mode 100644 test/docker/config-template.toml diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 0b92e8ce0..f4cb5514e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -33,6 +33,7 @@ - [p2p] \#3666 Add per channel telemetry to improve reactor observability - [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) - [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) +- [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` ### BUG FIXES: - [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) diff --git a/Makefile b/Makefile index 3dff35b9c..35baf7d3a 100644 --- a/Makefile +++ b/Makefile @@ -275,8 +275,8 @@ build-docker-localnode: @cd networks/local && make # Run a 4-node testnet locally -localnet-start: localnet-stop - @if ! [ -f build/node0/config/genesis.json ]; then docker run --rm -v $(CURDIR)/build:/tendermint:Z tendermint/localnode testnet --v 4 --o . --populate-persistent-peers --starting-ip-address 192.167.10.2 ; fi +localnet-start: localnet-stop build-docker-localnode + @if ! [ -f build/node0/config/genesis.json ]; then docker run --rm -v $(CURDIR)/build:/tendermint:Z tendermint/localnode testnet --config /etc/tendermint/config-template.toml --v 4 --o . --populate-persistent-peers --starting-ip-address 192.167.10.2; fi docker-compose up # Stop testnet diff --git a/config/config.go b/config/config.go index 6c4654fed..aa25cff6b 100644 --- a/config/config.go +++ b/config/config.go @@ -369,7 +369,7 @@ type RPCConfig struct { // DefaultRPCConfig returns a default configuration for the RPC server func DefaultRPCConfig() *RPCConfig { return &RPCConfig{ - ListenAddress: "tcp://0.0.0.0:26657", + ListenAddress: "tcp://127.0.0.1:26657", CORSAllowedOrigins: []string{}, CORSAllowedMethods: []string{"HEAD", "GET", "POST"}, CORSAllowedHeaders: []string{"Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"}, diff --git a/networks/local/localnode/Dockerfile b/networks/local/localnode/Dockerfile index 3942cecd6..03af5aa3c 100644 --- a/networks/local/localnode/Dockerfile +++ b/networks/local/localnode/Dockerfile @@ -13,4 +13,4 @@ CMD ["node", "--proxy_app", "kvstore"] STOPSIGNAL SIGTERM COPY wrapper.sh /usr/bin/wrapper.sh - +COPY config-template.toml /etc/tendermint/config-template.toml diff --git a/networks/local/localnode/config-template.toml b/networks/local/localnode/config-template.toml new file mode 100644 index 000000000..a90eb7bd5 --- /dev/null +++ b/networks/local/localnode/config-template.toml @@ -0,0 +1,2 @@ +[rpc] +laddr = "tcp://0.0.0.0:26657" diff --git a/test/docker/Dockerfile b/test/docker/Dockerfile index 77cc515e5..b39277bd9 100644 --- a/test/docker/Dockerfile +++ b/test/docker/Dockerfile @@ -27,7 +27,12 @@ RUN make install_abci # install Tendermint RUN make install -RUN tendermint testnet --node-dir-prefix="mach" --v=4 --populate-persistent-peers=false --o=$REPO/test/p2p/data +RUN tendermint testnet \ + --config $REPO/test/docker/config-template.toml \ + --node-dir-prefix="mach" \ + --v=4 \ + --populate-persistent-peers=false \ + --o=$REPO/test/p2p/data # Now copy in the code # NOTE: this will overwrite whatever is in vendor/ diff --git a/test/docker/config-template.toml b/test/docker/config-template.toml new file mode 100644 index 000000000..a90eb7bd5 --- /dev/null +++ b/test/docker/config-template.toml @@ -0,0 +1,2 @@ +[rpc] +laddr = "tcp://0.0.0.0:26657" From e6ab05125f8663895dd95c09e06d19d88a8470cc Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 24 Jun 2019 16:32:24 +0200 Subject: [PATCH 138/211] Added flags '-s -w' to buildflags (#3742) * Added flags '-s -w' to buildflags Signed-off-by: Marko Baricevic * added a condition for no strip * minor changes * on call discussions * on call discussion v2 --- Makefile | 3 ++- go.mod | 45 ++++++++++++++++++++++----------------------- go.sum | 24 ++++++++++++++++++++---- 3 files changed, 44 insertions(+), 28 deletions(-) diff --git a/Makefile b/Makefile index 35baf7d3a..f16e62560 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,8 @@ export GO111MODULE = on INCLUDE = -I=. -I=${GOPATH}/src -I=${GOPATH}/src/github.com/gogo/protobuf/protobuf BUILD_TAGS?='tendermint' -BUILD_FLAGS = -mod=readonly -ldflags "-X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD`" +LD_FLAGS = -X github.com/tendermint/tendermint/version.GitCommit=`git rev-parse --short=8 HEAD` -s -w +BUILD_FLAGS = -mod=readonly -ldflags "$(LD_FLAGS)" all: check build test install diff --git a/go.mod b/go.mod index 8fe1a124b..f4a4c6dda 100644 --- a/go.mod +++ b/go.mod @@ -3,50 +3,49 @@ module github.com/tendermint/tendermint go 1.12 require ( - github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 + github.com/BurntSushi/toml v0.3.1 // indirect + github.com/VividCortex/gohistogram v1.0.0 // indirect + github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 // indirect github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a - github.com/davecgh/go-spew v1.1.1 github.com/etcd-io/bbolt v1.3.2 github.com/fortytw2/leaktest v1.2.0 - github.com/fsnotify/fsnotify v1.4.7 github.com/go-kit/kit v0.6.0 github.com/go-logfmt/logfmt v0.3.0 - github.com/go-stack/stack v1.8.0 + github.com/go-stack/stack v1.8.0 // indirect github.com/gogo/protobuf v1.2.1 + github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect github.com/golang/protobuf v1.3.0 - github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db + github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect + github.com/google/gofuzz v1.0.0 // indirect github.com/gorilla/websocket v1.2.0 - github.com/hashicorp/hcl v1.0.0 - github.com/inconshreveable/mousetrap v1.0.0 + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmhodges/levigo v1.0.0 - github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 + github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect github.com/magiconair/properties v1.8.0 - github.com/matttproud/golang_protobuf_extensions v1.0.1 - github.com/mitchellh/mapstructure v1.1.2 - github.com/pelletier/go-toml v1.2.0 + github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect + github.com/mitchellh/mapstructure v1.1.2 // indirect + github.com/pelletier/go-toml v1.2.0 // indirect github.com/pkg/errors v0.8.0 - github.com/pmezard/go-difflib v1.0.0 github.com/prometheus/client_golang v0.9.1 - github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 - github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 - github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d + github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 // indirect + github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 // indirect + github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d // indirect github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 github.com/rs/cors v1.6.0 - github.com/spf13/afero v1.1.2 - github.com/spf13/cast v1.3.0 + github.com/spf13/afero v1.1.2 // indirect + github.com/spf13/cast v1.3.0 // indirect github.com/spf13/cobra v0.0.1 - github.com/spf13/jwalterweatherman v1.0.0 - github.com/spf13/pflag v1.0.3 + github.com/spf13/jwalterweatherman v1.0.0 // indirect + github.com/spf13/pflag v1.0.3 // indirect github.com/spf13/viper v1.0.0 github.com/stretchr/testify v1.2.2 github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e github.com/tendermint/go-amino v0.14.1 + go.etcd.io/bbolt v1.3.3 // indirect golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd - golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a - golang.org/x/text v0.3.0 - google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 + google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 // indirect google.golang.org/grpc v1.13.0 - gopkg.in/yaml.v2 v2.2.1 ) diff --git a/go.sum b/go.sum index fee691de9..2a349d300 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,13 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btcutil v0.0.0-20180524032703-d4cc87b86016/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= @@ -29,23 +32,31 @@ github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.0 h1:kbxbvI4Un1LUWKxufD+BiE6AEExYYgkQLQmLFqA1LFk= github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w= github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= @@ -54,7 +65,9 @@ github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5 github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= @@ -86,22 +99,22 @@ github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/viper v1.0.0 h1:RUA/ghS2i64rlnn4ydTfblY8Og8QzcPtCcHvgMn+w/I= github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= -github.com/stretchr/testify v1.2.1/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e h1:91EeXI4y4ShkyzkMqZ7QP/ZTIqwXp3RuDu5WFzxcFAs= github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/net v0.0.0-20180710023853-292b43bbf7cb/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= @@ -112,8 +125,11 @@ google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 h1:67iHsV9djwGdZpd google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/grpc v1.13.0 h1:bHIbVsCwmvbArgCJmLdgOdHFXlKqTOVjbibbS19cXHc= google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 0e7ff19e51f0f9a805ad61ad16e1b853704f83a8 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 24 Jun 2019 20:13:25 +0200 Subject: [PATCH 139/211] Added a disclaimer so the user is aware of binary (#3751) * Added a disaclaimer so the user is aware of binary - added disclaimer for `-s -w` Signed-off-by: Marko Baricevic * move up to compile --- docs/introduction/install.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/introduction/install.md b/docs/introduction/install.md index 4f35ffef7..00e04fa0b 100644 --- a/docs/introduction/install.md +++ b/docs/introduction/install.md @@ -45,6 +45,8 @@ make build to put the binary in `./build`. +_DISCLAIMER_ The binary of tendermint is build/installed without the DWARF symbol table. If you would like to build/install tendermint with the DWARF symbol and debug information, remove `-s -w` from `BUILD_FLAGS` in the make file. + The latest `tendermint version` is now installed. ## Run From 842d53096f15d30a06eab94aa8d0ef18d4116308 Mon Sep 17 00:00:00 2001 From: Mengjay Gao <1955889005@qq.com> Date: Tue, 25 Jun 2019 14:56:03 +0800 Subject: [PATCH 140/211] docs: update JS section of abci-cli.md (#3747) --- docs/app-dev/abci-cli.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/app-dev/abci-cli.md b/docs/app-dev/abci-cli.md index b09b9a11b..7e9db91b9 100644 --- a/docs/app-dev/abci-cli.md +++ b/docs/app-dev/abci-cli.md @@ -326,10 +326,18 @@ application easily in any language. We have implemented the counter in a number of languages [see the example directory](https://github.com/tendermint/tendermint/tree/develop/abci/example). -To run the Node JS version, `cd` to `example/js` and run +To run the Node.js version, fist download & install [the Javascript ABCI server](https://github.com/tendermint/js-abci): ``` -node app.js +git clone https://github.com/tendermint/js-abci.git +cd js-abci +npm install abci +``` + +Now you can start the app: + +```bash +node example/counter.js ``` (you'll have to kill the other counter application process). In another From 7b6073fa36751edd97f6471a0be487740f5f8ab4 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Tue, 25 Jun 2019 07:57:50 -0400 Subject: [PATCH 141/211] changelog and version (#3750) * changelog and version * Add section on ABCI changes * Update ABCI upgrade section to include events examples * update upgrading * more upgrading and changelog * update changelog from pending * refer to #rpc_changes * minor word changes --- CHANGELOG.md | 63 ++++++++++++++++++++++++++ CHANGELOG_PENDING.md | 23 +++------- UPGRADING.md | 102 ++++++++++++++++++++++++++++++++++++++++++- version/version.go | 2 +- 4 files changed, 171 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 068d99c3c..76f547a05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,68 @@ # Changelog +## v0.32.0 + +*June 25, 2019* + +Special thanks to external contributors on this release: +@needkane, @SebastianElvis, @andynog, @Yawning, @wooparadog + +This release contains breaking changes to our build and release processes, ABCI, +and the RPC, namely: +- Use Go modules instead of dep +- Bring active development to the `master` Github branch +- ABCI Tags are now Events - see + [docs](https://github.com/tendermint/tendermint/blob/60827f75623b92eff132dc0eff5b49d2025c591e/docs/spec/abci/abci.md#events) +- Bind RPC to localhost by default, not to the public interface [UPGRADING/RPC_Changes](./UPGRADING.md#rpc_changes) + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### BREAKING CHANGES: + +* CLI/RPC/Config + - [cli] \#3613 Switch from golang/dep to Go Modules to resolve dependencies: + It is recommended to switch to Go Modules if your project has tendermint as + a dependency. Read more on Modules here: + https://github.com/golang/go/wiki/Modules + - [config] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed `leveldb` as generic + option for `db_backend`. Must be `goleveldb` or `cleveldb`. + - [rpc] \#3616 Fix field names for `/block_results` response (eg. `results.DeliverTx` + -> `results.deliver_tx`). See docs for details. + - [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` + +* Apps + - [abci] \#1859 `ResponseCheckTx`, `ResponseDeliverTx`, `ResponseBeginBlock`, + and `ResponseEndBlock` now include `Events` instead of `Tags`. Each `Event` + contains a `type` and a list of `attributes` (list of key-value pairs) + allowing for inclusion of multiple distinct events in each response. + +* Go API + - [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI + Application interface + - [libs/db] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed deprecated `LevelDBBackend` const + If you have `db_backend` set to `leveldb` in your config file, please + change it to `goleveldb` or `cleveldb`. + - [p2p] \#3521 Remove NewNetAddressStringWithOptionalID + +* Blockchain Protocol + +* P2P Protocol + +### FEATURES: + +### IMPROVEMENTS: +- [abci/examples] \#3659 Change validator update tx format in the `persistent_kvstore` to use base64 for pubkeys instead of hex (@needkane) +- [consensus] \#3656 Exit if SwitchToConsensus fails +- [p2p] \#3666 Add per channel telemetry to improve reactor observability +- [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) + +### BUG FIXES: +- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) +- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) +- [node] \#3716 Fix a bug where `nil` is recorded as node's address +- [node] \#3741 Fix profiler blocking the entire node + ## v0.31.7 *June 3, 2019* diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f4cb5514e..849abb2e0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,7 +1,12 @@ -## v0.31.8 +## v0.32.1 ** +Special thanks to external contributors on this release: + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + ### BREAKING CHANGES: - \#3613 Switch from golang/dep to Go 1.11 Modules to resolve dependencies: @@ -10,17 +15,10 @@ - read more on Modules here: https://github.com/golang/go/wiki/Modules * CLI/RPC/Config -- [rpc] \#3616 Improve `/block_results` response format (`results.DeliverTx` -> - `results.deliver_tx`). See docs for details. * Apps * Go API -- [libs/db] Removed deprecated `LevelDBBackend` const - * If you have `db_backend` set to `leveldb` in your config file, please - change it to `goleveldb` or `cleveldb`. -- [p2p] \#3521 Remove NewNetAddressStringWithOptionalID -- [abci] \#3193 Use RequestDeliverTx and RequestCheckTx in the ABCI interface * Blockchain Protocol @@ -29,14 +27,5 @@ ### FEATURES: ### IMPROVEMENTS: -- [consensus] \#3656 Exit if SwitchToConsensus fails -- [p2p] \#3666 Add per channel telemetry to improve reactor observability -- [rpc] [\#3686](https://github.com/tendermint/tendermint/pull/3686) `HTTPClient#Call` returns wrapped errors, so a caller could use `errors.Cause` to retrieve an error code. (@wooparadog) -- [abci/examples] \#3659 Change validator update tx format (incl. expected pubkey format, which is base64 now) (@needkane) -- [rpc] \#3724 RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` ### BUG FIXES: -- [libs/db] \#3717 Fixed the BoltDB backend's Batch.Delete implementation (@Yawning) -- [libs/db] \#3718 Fixed the BoltDB backend's Get and Iterator implementation (@Yawning) -- [node] \#3716 Fix a bug where `nil` is recorded as node's address -- [node] \#3741 Fix profiler blocking the entire node diff --git a/UPGRADING.md b/UPGRADING.md index 5a77e0729..af42d2a66 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -3,14 +3,114 @@ This guide provides steps to be followed when you upgrade your applications to a newer version of Tendermint Core. - ## v0.32.0 +This release is compatible with previous blockchains, +however the new ABCI Events mechanism may create some complexity +for nodes wishing to continue operation with v0.32 from a previous version. +There are some minor breaking changes to the RPC. + ### Config Changes If you have `db_backend` set to `leveldb` in your config file, please change it to `goleveldb` or `cleveldb`. +### RPC Changes + +The default listen address for the RPC is now `127.0.0.1`. If you want to expose +it publicly, you have to explicitly configure it. Note exposing the RPC to the +public internet may not be safe - endpoints which return a lot of data may +enable resource exhaustion attacks on your node, causing the process to crash. + +Any consumers of `/block_results` need to be mindful of the change in all field +names from CamelCase to Snake case, eg. `results.DeliverTx` is now `results.deliver_tx`. +This is a fix, but it's breaking. + +### ABCI Changes + +ABCI responses which previously had a `Tags` field now have an `Events` field +instead. The original `Tags` field was simply a list of key-value pairs, where +each key effectively represented some attribute of an event occuring in the +blockchain, like `sender`, `receiver`, or `amount`. However, it was difficult to +represent the occurence of multiple events (for instance, multiple transfers) in a single list. +The new `Events` field contains a list of `Event`, where each `Event` is itself a list +of key-value pairs, allowing for more natural expression of multiple events in +eg. a single DeliverTx or EndBlock. Note each `Event` also includes a `Type`, which is meant to categorize the +event. + +For transaction indexing, the index key is +prefixed with the event type: `{eventType}.{attributeKey}`. +If the same event type and attribute key appear multiple times, the values are +appended in a list. + +To make queries, include the event type as a prefix. For instance if you +previously queried for `recipient = 'XYZ'`, and after the upgrade you name your event `transfer`, +the new query would be for `transfer.recipient = 'XYZ'`. + +Note that transactions indexed on a node before upgrading to v0.32 will still be indexed +using the old scheme. For instance, if a node upgraded at height 100, +transactions before 100 would be queried with `recipient = 'XYZ'` and +transactions after 100 would be queried with `transfer.recipient = 'XYZ'`. +While this presents additional complexity to clients, it avoids the need to +reindex. Of course, you can reset the node and sync from scratch to re-index +entirely using the new scheme. + +We illustrate further with a more complete example. + +Prior to the update, suppose your `ResponseDeliverTx` look like: + +```go +abci.ResponseDeliverTx{ + Tags: []cmn.KVPair{ + {Key: []byte("sender"), Value: []byte("foo")}, + {Key: []byte("recipient"), Value: []byte("bar")}, + {Key: []byte("amount"), Value: []byte("35")}, + } +} +``` + +The following queries would match this transaction: + +```go +query.MustParse("tm.event = 'Tx' AND sender = 'foo'") +query.MustParse("tm.event = 'Tx' AND recipient = 'bar'") +query.MustParse("tm.event = 'Tx' AND sender = 'foo' AND recipient = 'bar'") +``` + +Following the upgrade, your `ResponseDeliverTx` would look something like: +the following `Events`: + +```go +abci.ResponseDeliverTx{ + Events: []abci.Event{ + { + Type: "transfer", + Attributes: cmn.KVPairs{ + {Key: []byte("sender"), Value: []byte("foo")}, + {Key: []byte("recipient"), Value: []byte("bar")}, + {Key: []byte("amount"), Value: []byte("35")}, + }, + } +} +``` + +Now the following queries would match this transaction: + +```go +query.MustParse("tm.event = 'Tx' AND transfer.sender = 'foo'") +query.MustParse("tm.event = 'Tx' AND transfer.recipient = 'bar'") +query.MustParse("tm.event = 'Tx' AND transfer.sender = 'foo' AND transfer.recipient = 'bar'") +``` + +For further documentation on `Events`, see the [docs](https://github.com/tendermint/tendermint/blob/60827f75623b92eff132dc0eff5b49d2025c591e/docs/spec/abci/abci.md#events). + +### Go Applications + +The ABCI Application interface changed slightly so the CheckTx and DeliverTx +methods now take Request structs. The contents of these structs are just the raw +tx bytes, which were previously passed in as the argument. + + ## v0.31.6 There are no breaking changes in this release except Go API of p2p and diff --git a/version/version.go b/version/version.go index 1a15717f7..2d1c41fd5 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.31.7" + TMCoreSemVer = "0.32.0" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.0" From 17757a0b382d2eb7d7f102d9944936966c844297 Mon Sep 17 00:00:00 2001 From: zjubfd <296179868@qq.com> Date: Fri, 6 Sep 2019 15:17:21 +0800 Subject: [PATCH 142/211] [R4R] prepare for release v0.31.5-binance.2 (#113) * add index recovery * fix import order * add err judge * fix review sugest * add default abciResponses * change indexHeight to indexHeightKey * remove debug log improve performance of IsPersistent trim p2p metrics add BroadcastFromNonePersistent config to mempool remove unnecessary metrics rename config fix testcase * fix review * change log * [R4R]supoort hot sync reactor (#97) * supoort hot reactor fix peer lost issue fix metrics labels change parameters and log add more log try to answer even mute reduce redundant peers for candidate remove debug code do not deliver commit if miss blockId change sync pattern description avoid mix use of lock avoid timer leak decrease lock use fix current map read and write avoid message bust add more logs keep blockchain package stay fix import change default config fix review * use a single goroutine for switch * fix review suggestion * fix test case * fix pick error * move to decayed once picked freshset * update peerstate * fix pool test case * change issue num of pending log * use random with seed * use seed random * prepare for release v0.31.5-binance.2 (#112) --- CHANGELOG.md | 9 + blockchain/hot/candidate.go | 413 ++++++++++ blockchain/hot/candidate_test.go | 251 ++++++ blockchain/hot/metrics.go | 120 +++ blockchain/hot/pool.go | 1091 +++++++++++++++++++++++++++ blockchain/hot/pool_test.go | 604 +++++++++++++++ blockchain/hot/reactor.go | 365 +++++++++ blockchain/hot/reactor_test.go | 260 +++++++ blockchain/hot/wire.go | 13 + blockchain/pool.go | 2 +- blockchain/reactor.go | 68 +- blockchain/reactor_test.go | 17 +- blockchain/store_test.go | 15 + config/config.go | 44 +- config/toml.go | 17 +- consensus/reactor.go | 7 + consensus/state.go | 6 + consensus/types/round_state.go | 1 + mempool/mempool.go | 29 +- mempool/reactor.go | 56 +- node/node.go | 108 ++- p2p/conn/connection.go | 3 - p2p/peer.go | 7 - p2p/switch.go | 44 +- p2p/switch_test.go | 2 + rpc/core/pipe.go | 5 + rpc/core/status.go | 4 +- rpc/core/types/responses.go | 1 + snapshot/reactor.go | 28 +- state/blockindex/indexer_service.go | 9 + state/execution.go | 14 +- state/index.go | 181 +++++ state/index_test.go | 90 +++ state/metrics.go | 9 + state/store.go | 2 +- state/txindex/indexer_service.go | 9 + types/event_bus.go | 4 + types/events.go | 11 + types/vote_set.go | 15 + 39 files changed, 3751 insertions(+), 183 deletions(-) create mode 100644 blockchain/hot/candidate.go create mode 100644 blockchain/hot/candidate_test.go create mode 100644 blockchain/hot/metrics.go create mode 100644 blockchain/hot/pool.go create mode 100644 blockchain/hot/pool_test.go create mode 100644 blockchain/hot/reactor.go create mode 100644 blockchain/hot/reactor_test.go create mode 100644 blockchain/hot/wire.go create mode 100644 state/index.go create mode 100644 state/index_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index a4a1c2e66..ba671cdf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## v0.31.5-binance.2 +*Sep 6th, 2019* +### FEATURES: +- [sync] [\#97](https://github.com/binance-chain/bnc-tendermint/pull/97) supoort hot sync reactor + +### IMPROVEMENTS: +- [index] [\#106](https://github.com/binance-chain/bnc-tendermint/pull/106) index service recover from data lost +- [P2P] [\#106](https://github.com/binance-chain/bnc-tendermint/pull/107) introduce skip_tx_from_persistent config and other basic p2p improvement + ## v0.31.5-binance.1 *July 17th, 2019* diff --git a/blockchain/hot/candidate.go b/blockchain/hot/candidate.go new file mode 100644 index 000000000..9e4f03a44 --- /dev/null +++ b/blockchain/hot/candidate.go @@ -0,0 +1,413 @@ +package hot + +import ( + "container/list" + "math/rand" + "sort" + "sync" + "time" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/p2p" +) + +const ( + // the type of samples, Good means get sample result in limit time, otherwise is Bad event. + Bad eventType = iota + Good +) + +const ( + // the interval to recalculate average metrics, 10 is reasonable according to test. + recalculateInterval = 10 + // the interval to pick decay peers + pickDecayPeerInterval = 5000 + // only maxPermanentSetSize of best peers will stay in permanentSet + maxPermanentSetSize = 2 + // only recent maxMetricsSampleSize of samples will be used to calculate the metrics + maxMetricsSampleSize = 100 + + // the time interval to keep consistent with peerSet in Switch + compensateInterval = 2 +) + +type eventType uint + +type CandidatePool struct { + cmn.BaseService + mtx sync.RWMutex + random *rand.Rand + + pickSequence int64 + + // newly added peer, will be candidates in next pick round. + freshSet map[p2p.ID]struct{} + // unavailable or poor performance peers, give a try periodically + decayedSet map[p2p.ID]struct{} + // stable and good performance peers + permanentSet map[p2p.ID]*peerMetrics + + eventStream <-chan metricsEvent + + metrics *Metrics + swh *p2p.Switch +} + +func NewCandidatePool(eventStream <-chan metricsEvent) *CandidatePool { + random := rand.New(rand.NewSource(time.Now().Unix())) + c := &CandidatePool{ + eventStream: eventStream, + random: random, + // a random init value to avoid network resource race. + pickSequence: random.Int63n(pickDecayPeerInterval), + freshSet: make(map[p2p.ID]struct{}, 0), + decayedSet: make(map[p2p.ID]struct{}, 0), + permanentSet: make(map[p2p.ID]*peerMetrics, maxPermanentSetSize), + metrics: NopMetrics(), + } + c.BaseService = *cmn.NewBaseService(nil, "CandidatePool", c) + return c +} + +// OnStart implements cmn.Service. +func (c *CandidatePool) OnStart() error { + go c.handleSampleRoutine() + go c.compensatePeerRoutine() + + return nil +} + +func (c *CandidatePool) addPeer(pid p2p.ID) { + c.mtx.Lock() + defer c.mtx.Unlock() + c.freshSet[pid] = struct{}{} +} + +func (c *CandidatePool) removePeer(pid p2p.ID) { + c.mtx.Lock() + defer c.mtx.Unlock() + delete(c.freshSet, pid) + c.tryRemoveFromDecayed(pid) + c.tryRemoveFromPermanent(pid) +} + +func (c *CandidatePool) PickCandidates() []*p2p.ID { + c.mtx.Lock() + defer c.mtx.Unlock() + c.pickSequence = c.pickSequence + 1 + permanentPeer := c.pickFromPermanentPeer() + // if there is no permanentPeer, need broadcast in decayed peers. + isBroadcast := permanentPeer == nil + decaysPeers := c.pickFromDecayedSet(isBroadcast) + freshPeers := c.pickFromFreshSet() + peers := make([]*p2p.ID, 0, 1) + if permanentPeer != nil { + peers = append(peers, permanentPeer) + } + peers = append(peers, decaysPeers...) + peers = append(peers, freshPeers...) + return peers +} + +// The `AddPeer` and `RemovePeer` function of BaseReactor have concurrent issue: +// a peer is removed and immediately added, `AddPeer` may execute before `RemovePeer`. +// Can't fix this in `stopAndRemovePeer`, because may introduce other peer dial failed. +// For `CandidatePool` it is really important to keep consistent with peerset in switch, +// this routine is an insurance. +func (c *CandidatePool) compensatePeerRoutine() { + if c.swh == nil { + // happened in test + return + } + compensateTicker := time.NewTicker(compensateInterval) + defer compensateTicker.Stop() + for { + select { + case <-c.Quit(): + return + case <-compensateTicker.C: + func() { + c.mtx.Lock() + defer c.mtx.Unlock() + peers := c.swh.Peers().List() + for _, p := range peers { + if !c.exist(p.ID()) { + c.freshSet[p.ID()] = struct{}{} + } + } + for p := range c.permanentSet { + if !c.swh.Peers().Has(p) { + c.tryRemoveFromPermanent(p) + } + } + for p := range c.decayedSet { + if !c.swh.Peers().Has(p) { + c.tryRemoveFromDecayed(p) + } + } + for p := range c.freshSet { + if !c.swh.Peers().Has(p) { + delete(c.freshSet, p) + } + } + c.metrics.DecayPeerSetSize.Set(float64(len(c.decayedSet))) + c.metrics.PermanentPeerSetSize.Set(float64(len(c.permanentSet))) + }() + } + } +} + +func (c *CandidatePool) handleSampleRoutine() { + for { + select { + case <-c.Quit(): + return + case e := <-c.eventStream: + c.handleMetricsEvent(e) + } + } +} + +func (c *CandidatePool) handleMetricsEvent(e metricsEvent) { + c.mtx.Lock() + defer c.mtx.Unlock() + pid := e.pid + if !c.exist(pid) { + c.Logger.Debug("the peer is already removed", "peer", e.pid, "type", e.et) + return + } + if e.et == Bad { + if _, exist := c.decayedSet[pid]; !exist { + // just try to delete it, no need to check if it exist. + delete(c.freshSet, pid) + c.tryRemoveFromPermanent(pid) + c.decayedSet[pid] = struct{}{} + c.metrics.DecayPeerSetSize.Add(1) + } else { + // already in decayed set, nothing need to do. + } + } else if e.et == Good { + isFresh := c.isFresh(pid) + isDecayed := c.isDecayed(pid) + if isFresh || isDecayed { + if isFresh { + delete(c.freshSet, pid) + } + if isDecayed { + c.tryRemoveFromDecayed(pid) + } + metrics := newPeerMetrics(c.random) + metrics.addSample(e.dur) + added, kickPeer := c.tryAddPermanentPeer(pid, metrics) + if added { + c.Logger.Debug("new peer joined permanent peer set", "peer", pid) + } + // if some peers been kicked out, just join decayed peers. + if kickPeer != nil { + c.decayedSet[*kickPeer] = struct{}{} + c.metrics.DecayPeerSetSize.Add(1) + } + } else if metrics, exist := c.permanentSet[pid]; exist { + metrics.addSample(e.dur) + } else { + // should not happen + c.Logger.Error("receive event from peer which belongs to no peer set", "peer", e.pid) + } + } else { + c.Logger.Error("receive unknown metrics event type", "type", e.et) + } +} + +func (c *CandidatePool) tryAddPermanentPeer(pid p2p.ID, metrics peerMetrics) (bool, *p2p.ID) { + if _, exist := c.permanentSet[pid]; exist { + c.Logger.Error("try to insert a metrics that already exists", "peer", pid) + // unexpected things happened, the best choice is degrade this peer and try to fix. + c.tryRemoveFromPermanent(pid) + return false, &pid + } + if len(c.permanentSet) >= maxPermanentSetSize { + //try to kick out the worst candidate. + maxDelay := metrics.average + kickPeer := pid + for p, m := range c.permanentSet { + if maxDelay < m.average { + maxDelay = m.average + kickPeer = p + } + } + if kickPeer != pid { + c.tryRemoveFromPermanent(kickPeer) + c.permanentSet[pid] = &metrics + c.metrics.PermanentPeerSetSize.Add(1) + c.metrics.PermanentPeers.With("peer_id", string(pid)).Set(1) + return true, &kickPeer + } + return false, &pid + } else { + c.permanentSet[pid] = &metrics + c.metrics.PermanentPeerSetSize.Add(1) + c.metrics.PermanentPeers.With("peer_id", string(pid)).Set(1) + return true, nil + } +} + +func (c *CandidatePool) pickFromFreshSet() []*p2p.ID { + peers := make([]*p2p.ID, 0, len(c.freshSet)) + for peer := range c.freshSet { + // use a temp var to avoid append a same point. + tmpPeer := peer + // move to decayed once picked + if _, exist := c.decayedSet[tmpPeer]; !exist { + c.decayedSet[tmpPeer] = struct{}{} + c.metrics.DecayPeerSetSize.Add(float64(1)) + } + peers = append(peers, &tmpPeer) + } + // clean fresh set + c.freshSet = make(map[p2p.ID]struct{}, 0) + return peers +} + +func (c *CandidatePool) pickFromDecayedSet(broadcast bool) []*p2p.ID { + if len(c.decayedSet) == 0 { + return []*p2p.ID{} + } + peers := make([]*p2p.ID, 0, len(c.decayedSet)) + for peer := range c.decayedSet { + // use a temp var to avoid append a same point. + tmpPeer := peer + peers = append(peers, &tmpPeer) + } + if !broadcast { + if c.pickSequence%pickDecayPeerInterval == 0 { + index := c.random.Intn(len(peers)) + return []*p2p.ID{peers[index]} + } + return []*p2p.ID{} + } else { + return peers + } +} + +// larger average while less opportunity to be chosen. example: +// peer : peerA peerB peerC +// average delay: 1ns 1ns 8ns +// percentage : 0.1 0.1 0.8 +// diceSection : [0.9, 1.8, 2.0] +// choose opportunity: 45%, 45%, 10% +func (c *CandidatePool) pickFromPermanentPeer() *p2p.ID { + size := len(c.permanentSet) + if size == 0 { + return nil + } + diceSection := make([]float64, 0, size) + peers := make([]p2p.ID, 0, size) + var total, section float64 + for _, m := range c.permanentSet { + total += float64(m.average) + } + // total could not be 0 actually, but assign with 1 if it unfortunately happened + if total == 0 { + total = 1 + } + for p, m := range c.permanentSet { + peers = append(peers, p) + section = section + (1 - float64(m.average)/total) + diceSection = append(diceSection, section) + } + diceValue := c.random.Float64() * diceSection[len(diceSection)-1] + choose := sort.SearchFloat64s(diceSection, diceValue) + return &peers[choose] +} + +func (c *CandidatePool) exist(pid p2p.ID) bool { + if c.isPermanent(pid) { + return true + } else if c.isDecayed(pid) { + return true + } else if c.isFresh(pid) { + return true + } + return false +} + +func (c *CandidatePool) isFresh(pid p2p.ID) bool { + _, exist := c.freshSet[pid] + return exist +} + +func (c *CandidatePool) isDecayed(pid p2p.ID) bool { + _, exist := c.decayedSet[pid] + return exist +} + +func (c *CandidatePool) isPermanent(pid p2p.ID) bool { + _, exist := c.permanentSet[pid] + return exist +} + +func (c *CandidatePool) tryRemoveFromPermanent(pid p2p.ID) { + if c.isPermanent(pid) { + delete(c.permanentSet, pid) + c.metrics.PermanentPeerSetSize.Set(-1) + c.metrics.PermanentPeers.With("peer_id", string(pid)).Set(0) + } +} + +func (c *CandidatePool) tryRemoveFromDecayed(pid p2p.ID) { + if c.isDecayed(pid) { + delete(c.decayedSet, pid) + c.metrics.DecayPeerSetSize.Set(-1) + } +} + +// -------------------------------------------------- +type metricsEvent struct { + et eventType + pid p2p.ID + dur int64 // nano second +} + +type peerMetrics struct { + sampleSequence int64 + + samples *list.List + average int64 // nano second +} + +func newPeerMetrics(random *rand.Rand) peerMetrics { + return peerMetrics{ + samples: list.New(), + sampleSequence: random.Int63n(recalculateInterval), + } +} + +func (metrics *peerMetrics) addSample(dur int64) { + metrics.sampleSequence = metrics.sampleSequence + 1 + // fast calculate average, but error accumulates + if metrics.samples.Len() >= maxMetricsSampleSize { + // shift old sample + popItem := metrics.samples.Front() + metrics.samples.Remove(popItem) + popDur := *popItem.Value.(*int64) + // no worry about int64 overflow, the max dur will not exceed math.MaxInt64/maxMetricsSampleSize + metrics.average = (metrics.average*int64(maxMetricsSampleSize) + (dur - popDur)) / int64(maxMetricsSampleSize) + } else { + length := int64(metrics.samples.Len()) + metrics.average = (metrics.average*length + dur) / (length + 1) + } + metrics.samples.PushBack(&dur) + + // correction error periodically + if metrics.sampleSequence%recalculateInterval == 0 { + var totalDur int64 + // no worry about int64 overflow, the max dur will not exceed math.MaxInt64/maxMetricsSampleSize + s := metrics.samples.Front() + for s != nil { + sdur := *s.Value.(*int64) + totalDur += sdur + s = s.Next() + } + metrics.average = totalDur / int64(metrics.samples.Len()) + } +} diff --git a/blockchain/hot/candidate_test.go b/blockchain/hot/candidate_test.go new file mode 100644 index 000000000..d76665479 --- /dev/null +++ b/blockchain/hot/candidate_test.go @@ -0,0 +1,251 @@ +package hot + +import ( + "fmt" + "math" + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/p2p" +) + +func makeTestPeerId(num int) []p2p.ID { + if num <= 0 { + return []p2p.ID{} + } + pids := make([]p2p.ID, 0, num) + for i := 0; i < num; i++ { + pids = append(pids, p2p.ID(fmt.Sprintf("test id %v", i))) + } + return pids +} +func randomEvent() eventType { + if rand.Int()%2 == 0 { + return Good + } else { + return Bad + } +} +func TestPeerMetricsBasic(t *testing.T) { + pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) + seq := pm.sampleSequence + sample := rand.Int63() + pm.addSample(sample) + + assert.Equal(t, pm.average, sample) + assert.Equal(t, pm.samples.Len(), 1) + assert.Equal(t, pm.sampleSequence, seq+1) +} + +func TestPeerMetricsNoOverflow(t *testing.T) { + pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) + for i := 0; i < 10000; i++ { + // make sure it will not cause math overflow. + sample := rand.Int63n(math.MaxInt64 / maxMetricsSampleSize) + pm.addSample(sample) + } + assert.Equal(t, pm.samples.Len(), maxMetricsSampleSize) +} + +func TestPeerMetricsParamResonableInMillisecondLevel(t *testing.T) { + var sum int64 + pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) + for i := 0; i < recalculateInterval-1; i++ { + sample := rand.Int63n(recalculateInterval-1) * time.Millisecond.Nanoseconds() + sum += sample + pm.addSample(sample) + } + average := sum / (recalculateInterval - 1) + // the diff is not too much + assert.True(t, (average-pm.average) < time.Millisecond.Nanoseconds()/10 && (average-pm.average) < time.Millisecond.Nanoseconds()/10) + // the average is not too small + assert.True(t, average > 100) +} + +func TestPeerMetricsNoErrorAccumulate(t *testing.T) { + pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) + for i := 0; i < 10000; i++ { + sample := rand.Int63n(math.MaxInt64 / maxMetricsSampleSize) + pm.addSample(sample) + } + var sum int64 + // make sure it will recalculate when finish + pm.sampleSequence = 0 + for i := 0; i < maxMetricsSampleSize; i++ { + sample := rand.Int63n(math.MaxInt64 / maxMetricsSampleSize) + sum += sample + pm.addSample(sample) + } + average := sum / maxMetricsSampleSize + assert.Equal(t, pm.average, average) +} + +func TestCandidatePoolBasic(t *testing.T) { + sampleStream := make(chan metricsEvent) + candidatePool := NewCandidatePool(sampleStream) + candidatePool.Start() + defer candidatePool.Stop() + + // control the pick decay logic + candidatePool.pickSequence = 0 + totalPidNum := 85 + goodPidNum := 21 + + testPids := makeTestPeerId(totalPidNum) + for _, p := range testPids { + candidatePool.addPeer(p) + } + // peers stay in fresh set until an event come in + for i := 0; i < 2; i++ { + candidates := candidatePool.PickCandidates() + assert.Equal(t, len(candidates), totalPidNum) + } + + for i := 0; i < goodPidNum; i++ { + sampleStream <- metricsEvent{Good, testPids[i], int64(i) * time.Millisecond.Nanoseconds()} + } + for i := goodPidNum; i < totalPidNum; i++ { + sampleStream <- metricsEvent{Bad, testPids[i], 0} + } + //wait for pool to handle + time.Sleep(10 * time.Millisecond) + for i := 0; i < 2; i++ { + candidates := candidatePool.PickCandidates() + // only one peer is selected + assert.Equal(t, len(candidates), 1) + } + assert.Equal(t, len(candidatePool.permanentSet), maxPermanentSetSize) + for i := 0; i < maxPermanentSetSize; i++ { + _, exist := candidatePool.permanentSet[testPids[i]] + assert.True(t, exist) + } + assert.Equal(t, len(candidatePool.decayedSet), totalPidNum-maxPermanentSetSize) + assert.Equal(t, len(candidatePool.freshSet), 0) +} + +func TestCandidatePoolPickDecayPeriodically(t *testing.T) { + sampleStream := make(chan metricsEvent) + candidatePool := NewCandidatePool(sampleStream) + candidatePool.Start() + defer candidatePool.Stop() + testPids := makeTestPeerId(2) + candidatePool.addPeer(testPids[0]) + candidatePool.addPeer(testPids[1]) + sampleStream <- metricsEvent{Good, testPids[0], 1 * time.Millisecond.Nanoseconds()} + sampleStream <- metricsEvent{Bad, testPids[1], 0} + + //wait for pool to handle + time.Sleep(10 * time.Millisecond) + // control the pick decay logic + candidatePool.pickSequence = 0 + + for i := 0; i < pickDecayPeerInterval-1; i++ { + peers := candidatePool.PickCandidates() + assert.Equal(t, len(peers), 1) + } + peers := candidatePool.PickCandidates() + assert.Equal(t, len(peers), 2) +} + +func TestCandidatePoolNoDuplicatePeer(t *testing.T) { + sampleStream := make(chan metricsEvent) + candidatePool := NewCandidatePool(sampleStream) + candidatePool.Start() + defer candidatePool.Stop() + totalPidNum := 1000 + testPids := makeTestPeerId(totalPidNum) + for _, p := range testPids { + candidatePool.addPeer(p) + } + for i := 0; i < 100000; i++ { + dur := rand.Int63() + et := randomEvent() + peer := testPids[rand.Intn(totalPidNum)] + sampleStream <- metricsEvent{et, peer, dur} + } + time.Sleep(10 * time.Millisecond) + assert.Equal(t, len(candidatePool.freshSet)+len(candidatePool.decayedSet)+len(candidatePool.permanentSet), totalPidNum) +} + +func TestCandidatePoolPickInScore(t *testing.T) { + sampleStream := make(chan metricsEvent) + candidatePool := NewCandidatePool(sampleStream) + candidatePool.Start() + defer candidatePool.Stop() + totalPidNum := maxPermanentSetSize + testPids := makeTestPeerId(totalPidNum) + for i, p := range testPids { + candidatePool.addPeer(p) + sampleStream <- metricsEvent{Good, p, int64(i+1) * time.Millisecond.Nanoseconds()} + } + time.Sleep(10 * time.Millisecond) + candidatePool.PickCandidates() + pickRate := make(map[p2p.ID]int) + for i := 0; i < 100000; i++ { + peers := candidatePool.PickCandidates() + assert.Equal(t, 1, len(peers)) + peer := *peers[0] + pickRate[peer] = pickRate[peer] + 1 + } + for i := 0; i < maxPermanentSetSize; i++ { + fmt.Printf("index %d rate is %v\n", i, float64(pickRate[testPids[i]])/float64(100000)) + } + for i := 0; i < maxPermanentSetSize-1; i++ { + assert.True(t, pickRate[testPids[i]] > pickRate[testPids[i+1]]) + } +} + +func TestPickFromFreshSet(t *testing.T) { + sampleStream := make(chan metricsEvent) + candidatePool := NewCandidatePool(sampleStream) + candidatePool.Start() + defer candidatePool.Stop() + testPids := makeTestPeerId(100) + for _, p := range testPids { + candidatePool.addPeer(p) + } + candidates := candidatePool.pickFromFreshSet() + for _, c := range candidates { + for idx, tpid := range testPids { + if *c == tpid { + if len(testPids) > 1 { + testPids = append(testPids[:idx], testPids[idx+1:]...) + break + } else { + testPids = nil + break + } + } + } + } + assert.Nil(t, testPids) +} + +func TestPickFromDecayedSet(t *testing.T) { + sampleStream := make(chan metricsEvent) + candidatePool := NewCandidatePool(sampleStream) + candidatePool.Start() + defer candidatePool.Stop() + testPids := makeTestPeerId(100) + for _, p := range testPids { + candidatePool.addPeer(p) + sampleStream <- metricsEvent{Bad, p, 0} + } + candidates := candidatePool.pickFromDecayedSet(true) + for _, c := range candidates { + for idx, tpid := range testPids { + if *c == tpid { + if len(testPids) > 1 { + testPids = append(testPids[:idx], testPids[idx+1:]...) + break + } else { + testPids = nil + break + } + } + } + } + assert.Nil(t, testPids) +} diff --git a/blockchain/hot/metrics.go b/blockchain/hot/metrics.go new file mode 100644 index 000000000..9617ebf86 --- /dev/null +++ b/blockchain/hot/metrics.go @@ -0,0 +1,120 @@ +package hot + +import ( + "github.com/go-kit/kit/metrics" + "github.com/go-kit/kit/metrics/discard" + "github.com/go-kit/kit/metrics/prometheus" + stdprometheus "github.com/prometheus/client_golang/prometheus" +) + +const ( + // MetricsSubsystem is a subsystem shared by all metrics exposed by this + // package. + MetricsSubsystem = "hot_sync" +) + +// Metrics contains metrics exposed by this package. +type Metrics struct { + // Height of the chain. + Height metrics.Gauge + // Time between this and the last block. + BlockIntervalSeconds metrics.Gauge + // Number of transactions. + NumTxs metrics.Gauge + // Size of the block. + BlockSizeBytes metrics.Gauge + // Total number of transactions. + TotalTxs metrics.Gauge + // The latest block height. + CommittedHeight metrics.Gauge + + // The number of peers consider as good + PermanentPeerSetSize metrics.Gauge + // The detail peers consider as good + PermanentPeers metrics.Gauge + // The number of peers consider as bad + DecayPeerSetSize metrics.Gauge +} + +// PrometheusMetrics returns Metrics build using Prometheus client library. +// Optionally, labels can be provided along with their values ("foo", +// "fooValue"). +func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { + labels := []string{} + for i := 0; i < len(labelsAndValues); i += 2 { + labels = append(labels, labelsAndValues[i]) + } + return &Metrics{ + Height: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "height", + Help: "Height of the chain.", + }, labels).With(labelsAndValues...), + + BlockIntervalSeconds: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "block_interval_seconds", + Help: "Time between this and the last block.", + }, labels).With(labelsAndValues...), + + NumTxs: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "num_txs", + Help: "Number of transactions.", + }, labels).With(labelsAndValues...), + BlockSizeBytes: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "block_size_bytes", + Help: "Size of the block.", + }, labels).With(labelsAndValues...), + TotalTxs: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "total_txs", + Help: "Total number of transactions.", + }, labels).With(labelsAndValues...), + CommittedHeight: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "latest_block_height", + Help: "The latest block height.", + }, labels).With(labelsAndValues...), + PermanentPeerSetSize: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "permanent_set_size", + Help: "The number of peers consider as good.", + }, labels).With(labelsAndValues...), + DecayPeerSetSize: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "decay_peer_set_size", + Help: "The number of peers consider as bad", + }, labels).With(labelsAndValues...), + PermanentPeers: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "permanent_peers", + Help: "The details of peers consider as bad", + }, append(labels, "peer_id")).With(labelsAndValues...), + } +} + +// NopMetrics returns no-op Metrics. +func NopMetrics() *Metrics { + return &Metrics{ + Height: discard.NewGauge(), + BlockIntervalSeconds: discard.NewGauge(), + NumTxs: discard.NewGauge(), + BlockSizeBytes: discard.NewGauge(), + TotalTxs: discard.NewGauge(), + CommittedHeight: discard.NewGauge(), + PermanentPeerSetSize: discard.NewGauge(), + PermanentPeers: discard.NewGauge(), + DecayPeerSetSize: discard.NewGauge(), + } +} diff --git a/blockchain/hot/pool.go b/blockchain/hot/pool.go new file mode 100644 index 000000000..007351d36 --- /dev/null +++ b/blockchain/hot/pool.go @@ -0,0 +1,1091 @@ +package hot + +import ( + "bytes" + "context" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/tendermint/tendermint/blockchain" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + st "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" +) + +const ( + // SyncPattern is the work pattern of BlockPool. + // 1. Mute: will only answer subscribe requests from others, will not sync from others or from consensus channel. + // 2. Hot: handle subscribe requests from other peer as a publisher, also subscribe block messages + // from other peers as a subscriber. + // 3. Consensus: handle subscribe requests from other peer as a publisher, but subscribe block message from + // consensus channel. + // The viable transitions between are: + // Hot --> Consensus + // ^ ^ + // | / + // | / + // Mute + Mute SyncPattern = iota + Hot + Consensus +) + +const ( + // the time interval to correct current state. + tryRepairInterval = 1 * time.Second + + maxCachedSealedBlock = 100 + + // the max num of blocks can subscribed in advance. + maxSubscribeForesight = 40 + // the max num of blocks other peers can subscribe from us ahead of current height. + // should greater than maxSubscribeForesight. + maxPublishForesight = 80 + + maxCapPubFromStore = 1000 + + eventBusSubscribeCap = 1000 + + subscriber = "HotSyncService" + selfId = p2p.ID("self") +) + +type SyncPattern uint + +type BlockPool struct { + cmn.BaseService + mtx sync.Mutex + st SyncPattern + eventBus *types.EventBus + store *blockchain.BlockStore + blockExec *st.BlockExecutor + + blockTimeout time.Duration + + //--- state --- + // the verified blockState received recently + blockStates map[int64]*blockState + subscriberPeerSets map[int64]map[p2p.ID]struct{} + state st.State + blocksSynced int32 + subscribedHeight int64 + height int64 + + //--- internal communicate --- + blockStateSealCh chan *blockState + publisherStateSealCh chan *publisherState + decayedPeers chan decayedPeer + sendCh chan<- Message + pubFromStoreCh chan subFromStoreMsg + + //--- peer metrics --- + candidatePool *CandidatePool + sampleStream chan<- metricsEvent + + //--- other metrics -- + metrics *Metrics + + //--- for switch --- + switchCh chan struct{} + switchWg sync.WaitGroup +} + +func NewBlockPool(store *blockchain.BlockStore, blockExec *st.BlockExecutor, state st.State, sendCh chan<- Message, st SyncPattern, blockTimeout time.Duration) *BlockPool { + const capacity = 1000 + sampleStream := make(chan metricsEvent, capacity) + candidates := NewCandidatePool(sampleStream) + bp := &BlockPool{ + store: store, + height: state.LastBlockHeight, + blockExec: blockExec, + state: state, + subscribedHeight: state.LastBlockHeight, + blockStates: make(map[int64]*blockState, maxCachedSealedBlock), + subscriberPeerSets: make(map[int64]map[p2p.ID]struct{}, 0), + publisherStateSealCh: make(chan *publisherState, capacity), + sendCh: sendCh, + candidatePool: candidates, + sampleStream: sampleStream, + switchCh: make(chan struct{}, 1), + decayedPeers: make(chan decayedPeer, capacity), + blockStateSealCh: make(chan *blockState, capacity), + blockTimeout: blockTimeout, + st: st, + metrics: NopMetrics(), + pubFromStoreCh: make(chan subFromStoreMsg, maxCapPubFromStore), + } + bp.BaseService = *cmn.NewBaseService(nil, "HotBlockPool", bp) + return bp +} + +func (pool *BlockPool) setEventBus(eventBus *types.EventBus) { + pool.eventBus = eventBus +} + +func (pool *BlockPool) setMetrics(metrics *Metrics) { + pool.metrics = metrics + pool.candidatePool.metrics = metrics +} + +//---------------Service implement ---------------- +func (pool *BlockPool) OnStart() error { + if pool.st == Consensus { + pool.Logger.Info("block pool start in consensus pattern") + go pool.consensusSyncRoutine() + } else if pool.st == Hot { + pool.Logger.Info("block pool start in hotSync pattern") + pool.switchWg.Add(1) + pool.candidatePool.Start() + go pool.hotSyncRoutine() + } else { + pool.Logger.Info("block pool start in mute pattern") + } + go pool.pubFromStoreRoutine() + return nil +} + +func (pool *BlockPool) OnStop() { + if pool.candidatePool.IsRunning() { + pool.candidatePool.Stop() + } +} + +func (pool *BlockPool) SetLogger(l log.Logger) { + pool.BaseService.Logger = l.With("module", "blockPool") + pool.candidatePool.SetLogger(l.With("module", "candidatePool")) +} + +func (pool *BlockPool) AddPeer(peer p2p.Peer) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + // no matter what sync pattern now, need maintain candidatePool. + pool.candidatePool.addPeer(peer.ID()) +} + +func (pool *BlockPool) RemovePeer(peer p2p.Peer) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + // no matter what sync pattern now, need maintain candidatePool. + pool.candidatePool.removePeer(peer.ID()) + if pool.st == Hot { + pool.decayedPeers <- decayedPeer{peerId: peer.ID()} + } + for _, subscribers := range pool.subscriberPeerSets { + delete(subscribers, peer.ID()) + } +} + +// ------ switch logic ----- +func (pool *BlockPool) SwitchToHotSync(state st.State, blockSynced int32) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + if pool.st != Mute { + panic(fmt.Errorf("can't switch to hotsync pattern, current sync pattern is %d", pool.st)) + } + pool.Logger.Info("switch to hot sync pattern") + pool.st = Hot + pool.state = state + pool.subscribedHeight = state.LastBlockHeight + pool.blocksSynced = blockSynced + pool.height = state.LastBlockHeight + + pool.switchWg.Add(1) + pool.candidatePool.Start() + go pool.hotSyncRoutine() +} + +func (pool *BlockPool) SwitchToConsensusSync(state *st.State) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + if pool.st == Consensus { + panic("already in consensus sync, can't switch to consensus sync") + } + var copyState st.State + // means switch from hot sync + if state == nil { + copyState = pool.state + } else { + copyState = state.Copy() + } + pool.switchCh <- struct{}{} + // wait until hotSyncRoutine ends. + pool.switchWg.Wait() + // clean block states from expecting height. + pool.resetBlockStates() + + pool.Logger.Info("switch to consensus sync pattern") + pool.st = Consensus + pool.state = copyState + pool.height = copyState.LastBlockHeight + pool.subscribedHeight = copyState.LastBlockHeight + + if pool.candidatePool.IsRunning() { + pool.candidatePool.Stop() + } + go pool.consensusSyncRoutine() +} + +//------- handle request from other peer ----- +func (pool *BlockPool) handleSubscribeBlock(fromHeight, toHeight int64, src p2p.Peer) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + if pool.st == Mute { + for height := fromHeight; height <= toHeight; height++ { + if height < pool.store.Height() { + select { + case pool.pubFromStoreCh <- subFromStoreMsg{height: height, src: src}: + default: + pool.Logger.Error("the pubFromStoreCh channel is full, skip loading block from store", "peer", src.ID(), "height", height) + return + } + } else { + // Won't answer no block message immediately. + // Afraid of the peer will subscribe again immediately, which + // will cause message burst. + return + } + } + return + } + for height := fromHeight; height <= toHeight; height++ { + blockState := pool.blockStates[height] + if blockState != nil && blockState.isSealed() { + pool.sendMessages(src.ID(), &blockCommitResponseMessage{blockState.block, blockState.commit}) + } else if height <= pool.currentHeight() { + select { + case pool.pubFromStoreCh <- subFromStoreMsg{height: height, src: src}: + default: + pool.Logger.Error("the pubFromStoreCh channel is full, skip loading block from store", "peer", src.ID(), "height", height) + return + } + } else if height < pool.currentHeight()+maxPublishForesight { + // even if the peer have subscribe at the height before, in consideration of + // network/protocol robustness, still sent what we have again. + if blockState != nil { + if blockState.latestBlock != nil { + pool.sendMessages(src.ID(), &blockCommitResponseMessage{Block: blockState.latestBlock}) + } + if blockState.latestCommit != nil { + pool.sendMessages(src.ID(), &blockCommitResponseMessage{Commit: blockState.latestCommit}) + } + } + pool.Logger.Debug("handle peer subscribe block", "peer", src.ID(), "height", height) + if peers, exist := pool.subscriberPeerSets[height]; exist { + peers[src.ID()] = struct{}{} + } else { + pool.subscriberPeerSets[height] = map[p2p.ID]struct{}{src.ID(): {}} + } + } else { + // will not handle higher subscribe request + pool.sendMessages(src.ID(), &noBlockResponseMessage{height}) + return + } + } + return +} + +func (pool *BlockPool) handleUnSubscribeBlock(height int64, src p2p.Peer) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + if pool.st == Mute { + return + } + pool.Logger.Debug("handle peer unsubscribe block", "peer", src.ID(), "height", height) + if subscribers, exist := pool.subscriberPeerSets[height]; exist { + delete(subscribers, src.ID()) + } +} + +func (pool *BlockPool) handleBlockCommit(block *types.Block, commit *types.Commit, src p2p.Peer) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + if pool.st != Hot { + return + } + var height int64 + // have checked, block and commit can't both be nil + if block != nil { + height = block.Height + } else { + height = commit.Height() + } + if ps := pool.getPublisherAtHeight(height, src.ID()); ps != nil { + if block != nil { + ps.receiveBlock(block) + } + if commit != nil { + ps.receiveCommit(commit) + } + } else { + pool.Logger.Info("receive block/commit that is not expected", "peer", src.ID(), "height", height) + } +} + +func (pool *BlockPool) handleNoBlock(height int64, src p2p.Peer) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + if pool.st != Hot { + return + } + if ps := pool.getPublisherAtHeight(height, src.ID()); ps != nil { + ps.receiveNoBlock() + } else { + pool.Logger.Info("receive no block message that is not expected", "peer", src.ID(), "height", height) + } +} + +func (pool *BlockPool) handleBlockFromSelf(height int64, block *types.Block) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + bc, exist := pool.blockStates[height] + if !exist { + bc = pool.newBlockStateAtHeight(height) + } + ps, exist := bc.pubStates[selfId] + if !exist { + ps = NewPeerPublisherState(bc, selfId, pool.Logger, pool.blockTimeout, pool) + bc.pubStates[selfId] = ps + } + ps.receiveBlock(block) +} + +func (pool *BlockPool) handleCommitFromSelf(height int64, commit *types.Commit) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + bc, exist := pool.blockStates[height] + if !exist { + bc = pool.newBlockStateAtHeight(height) + } + ps, exist := bc.pubStates[selfId] + if !exist { + ps = NewPeerPublisherState(bc, selfId, pool.Logger, pool.blockTimeout, pool) + bc.pubStates[selfId] = ps + } + ps.receiveCommit(commit) +} + +// ------------------------------------------------- +func (pool *BlockPool) hotSyncRoutine() { + tryRepairTicker := time.NewTicker(tryRepairInterval) + defer tryRepairTicker.Stop() + pool.subscribeBlockInForesightExclusive() + for { + if !pool.IsRunning() { + break + } + select { + case bs := <-pool.blockStateSealCh: + pool.compensatePublish(bs) + pool.applyBlock(bs) + pool.recordMetrics(bs.block) + pool.incBlocksSynced() + pool.incCurrentHeight() + pool.trimStaleState() + // try subscribe more + pool.subscribeBlockInForesightExclusive() + pool.wakeupNextBlockState() + case ps := <-pool.publisherStateSealCh: + pool.updatePeerMetrics(ps) + if ps.broken { + pool.decayedPeers <- decayedPeer{ps.pid, ps.bs.height} + } else { + pool.updatePeerState(ps.pid) + } + case peer := <-pool.decayedPeers: + pool.tryReschedule(peer) + case <-tryRepairTicker.C: + pool.tryRepair() + case <-pool.switchCh: + pool.Logger.Info("stopping hotsync routine") + pool.switchWg.Done() + return + } + } +} + +func (pool *BlockPool) pubFromStoreRoutine() { + for { + select { + case <-pool.Quit(): + return + case subMsg := <-pool.pubFromStoreCh: + pool.sendBlockFromStore(subMsg.height, subMsg.src) + } + } +} + +func (pool *BlockPool) consensusSyncRoutine() { + blockSub, err := pool.eventBus.Subscribe(context.Background(), subscriber, types.EventQueryCompleteProposal, eventBusSubscribeCap) + if err != nil { + panic(err) + } + commitSub, err := pool.eventBus.Subscribe(context.Background(), subscriber, types.EventQueryMajorPrecommits, eventBusSubscribeCap) + if err != nil { + panic(err) + } + for { + if !pool.IsRunning() { + break + } + select { + case bs := <-pool.blockStateSealCh: + // there is no guarantee that this routine happens before consensus reactor start. + // should tolerate miss some blocks at first. + // so we use setCurrentHeight instead of incHeight. + pool.setCurrentHeight(bs.height) + pool.compensatePublish(bs) + pool.trimStaleState() + case <-pool.publisherStateSealCh: + // just drain the channel + case blockData := <-blockSub.Out(): + blockProposal := blockData.Data().(types.EventDataCompleteProposal) + block := blockProposal.Block + height := blockProposal.Height + pool.handleBlockFromSelf(height, &block) + case commitData := <-commitSub.Out(): + precommit := commitData.Data().(types.EventDataMajorPrecommits) + // if maj23 is zero, means will have another round. + if maj23, ok := precommit.Votes.TwoThirdsMajority(); ok && !maj23.IsZero() { + height := precommit.Height + commit := precommit.Votes.MakeCommit() + pool.handleCommitFromSelf(height, commit) + } + } + } + +} + +//------------------------------------- +func (pool *BlockPool) resetBlockStates() { + for height := pool.expectingHeight(); height <= pool.subscribedHeight; height++ { + if pubs := pool.getPublishersAtHeight(height); pubs != nil { + for _, ps := range pubs { + if ps.timeout != nil { + ps.timeout.Stop() + } + if !ps.sealed { + pool.sendMessages(ps.pid, &blockUnSubscribeMessage{Height: ps.bs.height}) + } + } + } + delete(pool.blockStates, height) + } +} + +func (pool *BlockPool) tryReschedule(peer decayedPeer) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + height := peer.fromHeight + if height == 0 { + height = pool.expectingHeight() + } + pool.Logger.Info("try reschedule for block", "from_height", height, "to_height", pool.subscribedHeight, "peer", peer.peerId) + for ; height <= pool.subscribedHeight; height++ { + if pubStates := pool.getPublishersAtHeight(height); pubStates != nil { + if ps, exist := pubStates[peer.peerId]; exist { + if ps.timeout != nil { + ps.timeout.Stop() + } + // if the peer is removed, try send unsubscribeMessage actually will do nothing. + pool.sendMessages(peer.peerId, &blockUnSubscribeMessage{Height: height}) + delete(pubStates, ps.pid) + // is is possible the current height blockstate is sealed, but current height is not update yet. + // if it is sealed, just skip + if !ps.bs.isSealed() && len(pubStates) == 0 { + // empty now, choose new peers for this height. + pool.subscribeBlockAtHeight(height, nil) + } + } + } + } +} + +func (pool *BlockPool) tryRepair() { + pool.mtx.Lock() + defer pool.mtx.Unlock() + // have't block any blocks yet. + if pool.currentHeight() == pool.subscribedHeight { + pool.subscribeBlockInForesight() + } else { + // reschedule may failed because of no peer available, need subscribe again during repair + for h := pool.expectingHeight(); h <= pool.subscribedHeight; h++ { + if pubStates := pool.getPublishersAtHeight(h); len(pubStates) == 0 { + pool.Logger.Info("try resubscribe for block", "height", h) + pool.subscribeBlockAtHeight(h, nil) + } + } + } +} + +func (pool *BlockPool) wakeupNextBlockState() { + pool.mtx.Lock() + defer pool.mtx.Unlock() + pool.Logger.Info("Wake next block", "height", pool.expectingHeight()) + if bs := pool.blockStates[pool.expectingHeight()]; bs != nil { + // new round, sent new start time + bs.startTime = time.Now() + if pubs := pool.getPublishersAtHeight(pool.expectingHeight()); pubs != nil { + for _, ps := range pubs { + ps.wakeup() + } + } + } +} + +func (pool *BlockPool) sendBlockFromStore(height int64, src p2p.Peer) { + // skip load block when the peer is already stopped. + if !src.IsRunning() { + return + } + pool.Logger.Debug("send block from store", "peer", src.ID(), "height", height) + block := pool.store.LoadBlock(height) + if block == nil { + pool.sendMessages(src.ID(), &noBlockResponseMessage{height}) + return + } + blockAfter := pool.store.LoadBlock(height + 1) + if blockAfter == nil { + pool.sendMessages(src.ID(), &noBlockResponseMessage{height}) + return + } + pool.sendMessages(src.ID(), &blockCommitResponseMessage{Block: block, Commit: blockAfter.LastCommit}) +} + +// applyBlock takes most of time, make thin lock in updateState. +func (pool *BlockPool) applyBlock(bs *blockState) { + pool.Logger.Info("Apply block", "height", bs.block.Height) + blockParts := bs.block.MakePartSet(types.BlockPartSizeBytes) + pool.store.SaveBlock(bs.block, blockParts, bs.commit) + // get the hash without persisting the state + newState, err := pool.blockExec.ApplyBlock(pool.state, *bs.blockId, bs.block) + if err != nil { + panic(fmt.Sprintf("Failed to process committed blockChainMessage (%d:%X): %v", bs.block.Height, bs.block.Hash(), err)) + } + pool.updateState(newState) +} + +func (pool *BlockPool) updateState(state st.State) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + pool.state = state +} + +// the subscribe strategy: +// +// one candidate: +// 1. if `subscribedHeight-currentHeight > maxSubscribeForesight/2`, means have subscribe many blocks now, do nothing +// 2. else [subscribedHeight + 1, currentHeight+maxSubscribeForesight] subscribe request will be send. +// +// more than one candidates: only block at `subscribedHeight + 1` will be subscribed. +func (pool *BlockPool) subscribeBlockInForesight() { + peers := pool.candidatePool.PickCandidates() + if len(peers) == 0 { + pool.Logger.Error("no peers is available", "height", pool.currentHeight()) + return + } + var fromHeight, toHeight int64 + if len(peers) == 1 { + peer := peers[0] + if pool.subscribedHeight-pool.currentHeight() > maxSubscribeForesight/2 { + return + } else { + fromHeight = pool.subscribedHeight + 1 + toHeight = pool.currentHeight() + maxSubscribeForesight + } + for h := fromHeight; h <= toHeight; h++ { + bc := pool.newBlockStateAtHeight(h) + bc.pubStates[*peer] = NewPeerPublisherState(bc, *peer, pool.Logger, pool.blockTimeout, pool) + } + pool.sendMessages(*peer, &blockSubscribeMessage{fromHeight, toHeight}) + pool.subscribedHeight = toHeight + return + } else if len(peers) > 1 { + if pool.subscribedHeight+1 <= pool.currentHeight()+maxSubscribeForesight { + height := pool.subscribedHeight + 1 + pool.subscribeBlockAtHeight(height, peers) + pool.subscribedHeight = height + } + return + } +} + +func (pool *BlockPool) subscribeBlockInForesightExclusive() { + pool.mtx.Lock() + defer pool.mtx.Unlock() + pool.subscribeBlockInForesight() + +} + +func (pool *BlockPool) newBlockStateAtHeight(height int64) *blockState { + newBc := NewBlockState(height, pool.Logger, pool) + pool.blockStates[height] = newBc + return newBc +} + +func (pool *BlockPool) subscribeBlockAtHeight(height int64, peers []*p2p.ID) { + if peers == nil { + peers = pool.candidatePool.PickCandidates() + } + if len(peers) == 0 { + pool.Logger.Error("no peers is available", "height", pool.currentHeight()) + return + } + bc := pool.newBlockStateAtHeight(height) + for _, peer := range peers { + ps := NewPeerPublisherState(bc, *peer, pool.Logger, pool.blockTimeout, pool) + bc.pubStates[*peer] = ps + pool.sendMessages(*peer, &blockSubscribeMessage{height, height}) + } +} + +// May publish order and commit in incorrect order, try correct it once we know the true block and commit. +// It is expensive to maintain all received blocks before receiving a +// valid commit, this is a trade off between complexity, memory and network. +func (pool *BlockPool) compensatePublish(bs *blockState) { + pool.mtx.Lock() + defer pool.mtx.Unlock() + if !bytes.Equal(bs.block.Hash(), bs.latestBlock.Hash()) { + bs.pool.publishBlock(bs.height, bs.block) + } + if !bytes.Equal(bs.commit.Hash(), bs.latestCommit.Hash()) { + bs.pool.publishCommit(bs.height, bs.commit) + } + delete(bs.pool.subscriberPeerSets, bs.height) +} + +func (pool *BlockPool) publishBlock(height int64, block *types.Block) { + if subscribers := pool.subscriberPeerSets[height]; len(subscribers) > 0 { + for p := range subscribers { + pool.sendMessages(p, &blockCommitResponseMessage{Block: block}) + } + } +} + +func (pool *BlockPool) publishCommit(height int64, commit *types.Commit) { + if subscribers := pool.subscriberPeerSets[height]; len(subscribers) > 0 { + for p := range subscribers { + pool.sendMessages(p, &blockCommitResponseMessage{Commit: commit}) + } + } +} + +func (pool *BlockPool) trimStaleState() { + pool.mtx.Lock() + defer pool.mtx.Unlock() + trimHeight := pool.currentHeight() - maxCachedSealedBlock + if trimHeight > 0 { + if pubStates := pool.getPublishersAtHeight(trimHeight); pubStates != nil { + for _, ps := range pubStates { + if pool.st == Hot { + ps.tryExpire() + } + } + } + delete(pool.blockStates, trimHeight) + } +} + +func (pool *BlockPool) updatePeerState(pid p2p.ID) { + if pool.candidatePool.swh == nil || pool.candidatePool.swh.Peers() == nil { + return + } + if p := pool.candidatePool.swh.Peers().Get(pid); p != nil { + peerState, ok := p.Get(types.PeerStateKey).(peerState) + if !ok { + // Peer does not have a state yet. It is set in the consensus reactor. + return + } + peerState.SetHeight(pool.expectingHeight()) + } +} + +func (pool *BlockPool) getPublishersAtHeight(height int64) map[p2p.ID]*publisherState { + if bs, exist := pool.blockStates[height]; exist { + return bs.pubStates + } + return nil +} + +func (pool *BlockPool) getPublisherAtHeight(height int64, pid p2p.ID) *publisherState { + if pubs := pool.getPublishersAtHeight(height); pubs != nil { + return pubs[pid] + } + return nil +} + +// where use currentHeight have already locked +func (pool *BlockPool) currentHeight() int64 { + return pool.height +} + +func (pool *BlockPool) incCurrentHeight() { + pool.mtx.Lock() + pool.height++ + pool.mtx.Unlock() +} + +func (pool *BlockPool) setCurrentHeight(height int64) { + pool.mtx.Lock() + pool.height = height + pool.mtx.Unlock() +} + +// most of the usage have locked. Only newBlockStateAtHeight do not, but the usage can't +// currently excuted with incCurrentHeight, it is safe. +func (pool *BlockPool) expectingHeight() int64 { + return pool.height + 1 +} + +func (pool *BlockPool) incBlocksSynced() int32 { + return atomic.AddInt32(&pool.blocksSynced, 1) +} + +func (pool *BlockPool) getBlockSynced() int32 { + return atomic.LoadInt32(&pool.blocksSynced) +} + +func (pool *BlockPool) getSyncPattern() SyncPattern { + pool.mtx.Lock() + defer pool.mtx.Unlock() + return pool.st +} + +func (pool *BlockPool) verifyCommit(blockID types.BlockID, commit *types.Commit) error { + return pool.state.Validators.VerifyCommit(pool.state.ChainID, blockID, pool.currentHeight()+1, commit) +} + +func (pool *BlockPool) sendMessages(pid p2p.ID, msgs ...BlockchainMessage) { + for _, msg := range msgs { + select { + case pool.sendCh <- Message{msg, pid}: + default: + pool.Logger.Error("Failed to send commit/blockChainMessage since messagesCh is full", "peer", pid) + } + } +} + +func (pool *BlockPool) updatePeerMetrics(ps *publisherState) { + var et eventType + var dur int64 + if ps.broken { + et = Bad + } else { + et = Good + dur = time.Now().Sub(ps.bs.startTime).Nanoseconds() + // this should not happened, but defend it. + if dur <= 0 { + dur = 1 + } + } + select { + case ps.pool.sampleStream <- metricsEvent{et, ps.pid, dur}: + default: + ps.logger.Error("failed to send good sample event", "peer", ps.pid, "height", ps.bs.height) + } + return +} + +func (pool *BlockPool) recordMetrics(block *types.Block) { + height := block.Height + if height > 1 { + var lastBlockTime time.Time + if lastBs, exist := pool.blockStates[height-1]; exist { + lastBlockTime = lastBs.block.Time + } else { + lastBlockTime = pool.store.LoadBlockMeta(height - 1).Header.Time + } + pool.metrics.BlockIntervalSeconds.Set( + block.Time.Sub(lastBlockTime).Seconds(), + ) + } + pool.metrics.NumTxs.Set(float64(block.NumTxs)) + pool.metrics.BlockSizeBytes.Set(float64(block.Size())) + pool.metrics.TotalTxs.Set(float64(block.TotalTxs)) + pool.metrics.CommittedHeight.Set(float64(height)) + pool.metrics.Height.Set(float64(height + 1)) +} + +//---------------------------------------- +// blockState track the response of multi peers about block/commit at specified +// height that blockPool subscribed. +type blockState struct { + //--- ops fields + mux sync.Mutex + logger log.Logger + startTime time.Time + + pool *BlockPool + sealed bool + + //--- data fields + height int64 + commit *types.Commit + block *types.Block + blockId *types.BlockID + + // recently received commit and blockChainMessage + latestCommit *types.Commit + latestBlock *types.Block + + pubStates map[p2p.ID]*publisherState +} + +func NewBlockState(height int64, logger log.Logger, pool *BlockPool) *blockState { + bc := &blockState{ + height: height, + logger: logger, + pool: pool, + pubStates: make(map[p2p.ID]*publisherState, 0), + } + if pool.expectingHeight() == height { + bc.startTime = time.Now() + } + return bc +} + +func (bs *blockState) setLatestBlock(block *types.Block) { + bs.mux.Lock() + defer bs.mux.Unlock() + if bs.sealed { + return + } + if bs.latestBlock == nil || !bytes.Equal(bs.latestBlock.Hash(), block.Hash()) { + bs.latestBlock = block + // notice blockChainMessage state change + bs.pool.publishBlock(bs.height, block) + } +} + +func (bs *blockState) setLatestCommit(commit *types.Commit) { + bs.mux.Lock() + defer bs.mux.Unlock() + if bs.sealed { + return + } + if bs.latestCommit == nil || !bytes.Equal(bs.latestCommit.Hash(), commit.Hash()) { + bs.latestCommit = commit + // notice blockChainMessage state change + bs.pool.publishCommit(bs.height, commit) + } +} + +func (bs *blockState) seal() { + bs.mux.Lock() + defer bs.mux.Unlock() + if bs.sealed == true { + return + } + bs.sealed = true + bs.logger.Debug("sealing blockChainMessage commit", "height", bs.height) + bs.pool.blockStateSealCh <- bs +} + +func (bs *blockState) isSealed() bool { + bs.mux.Lock() + defer bs.mux.Unlock() + return bs.sealed +} + +//---------------------------------------- +// publisherState is used to track the continuous response of a peer about block/commit at specified +// height that blockpool subscribed. The peer plays as a publisher. +type publisherState struct { + mux sync.Mutex + logger log.Logger + timeoutDur time.Duration + + bs *blockState + pool *BlockPool + + timeout *time.Timer + isWake bool + + broken bool + sealed bool + + pid p2p.ID + commit *types.Commit + block *types.Block +} + +func NewPeerPublisherState(bc *blockState, pid p2p.ID, logger log.Logger, timeoutDur time.Duration, pool *BlockPool) *publisherState { + ps := &publisherState{ + logger: logger, + pid: pid, + bs: bc, + pool: pool, + timeoutDur: timeoutDur, + } + if pool.expectingHeight() == bc.height || pid == selfId { + if pool.st == Hot { + ps.startTimer() + } + ps.isWake = true + } + return ps +} + +func (ps *publisherState) wakeup() { + ps.mux.Lock() + defer ps.mux.Unlock() + ps.startTimer() + ps.isWake = true + ps.trySeal() +} + +// if the ps still not trig timeout, but is going to trim, +// consider it as bad peer, and update metrics by itself. +func (ps *publisherState) tryExpire() { + ps.mux.Lock() + defer ps.mux.Unlock() + if !ps.sealed { + // stop the timer in case it leaks + if ps.timeout != nil { + ps.timeout.Stop() + } + ps.broken = true + ps.pool.updatePeerMetrics(ps) + } +} + +func (ps *publisherState) onTimeout() { + ps.mux.Lock() + defer ps.mux.Unlock() + if ps.sealed { + return + } + ps.broken = true + ps.seal() +} + +func (ps *publisherState) receiveBlock(block *types.Block) { + ps.mux.Lock() + defer ps.mux.Unlock() + if ps.sealed { + ps.logger.Debug("received blockChainMessage that is already sealed", "peer", ps.pid, "height", ps.bs.height) + return + } + ps.block = block + ps.bs.setLatestBlock(block) + ps.trySeal() +} + +func (ps *publisherState) receiveNoBlock() { + ps.mux.Lock() + defer ps.mux.Unlock() + if ps.sealed { + return + } + ps.broken = true + ps.seal() +} + +func (ps *publisherState) receiveCommit(commit *types.Commit) { + ps.mux.Lock() + defer ps.mux.Unlock() + if ps.sealed { + ps.logger.Debug("received commit that is already sealed", "peer", ps.pid, "height", ps.bs.height) + return + } + ps.commit = commit + ps.bs.setLatestCommit(commit) + ps.trySeal() +} + +func (ps *publisherState) trySeal() { + if ps.bs.isSealed() { + ps.tryLaterSeal() + } else { + ps.tryFirstSeal() + } +} + +func (ps *publisherState) tryLaterSeal() { + if ps.block == nil || ps.commit == nil { + return + } + blockId := makeBlockID(ps.block) + // For a later sealed request, just choose to compare the the blockChainMessage id to + // decide whether it is a good peer. Fully verification is costly and + // no strong need to do so since we only used to judge if the peer is good or not. + if blockId.Equals(ps.commit.BlockID) && blockId.Equals(*ps.bs.blockId) { + ps.seal() + } else { + // maybe blockChainMessage is late, won't seal. + ps.logger.Info("received inconsistent blockChainMessage/commit", "peer", ps.pid, "height", ps.bs.height) + } +} + +func (ps *publisherState) tryFirstSeal() { + if ps.block == nil || ps.commit == nil { + return + } + // not now + if !ps.isWake { + return + } + blockId := makeBlockID(ps.block) + if ps.pid == selfId { + if !blockId.Equals(ps.commit.BlockID) { + return + } + } else if err := ps.pool.verifyCommit(*blockId, ps.commit); err != nil { + // maybe blockChainMessage is late, won't seal. + ps.logger.Info("received inconsistent blockChainMessage/commit", "peer", ps.pid, ps.bs.height) + return + } + ps.bs.commit = ps.commit + ps.bs.block = ps.block + ps.bs.blockId = blockId + ps.seal() + ps.bs.seal() + return +} + +// only start timer when blockpool reach at this height. +func (ps *publisherState) startTimer() { + if ps.timeout != nil { + ps.timeout.Reset(ps.timeoutDur) + } else { + ps.timeout = time.AfterFunc(ps.timeoutDur, ps.onTimeout) + } +} + +func (ps *publisherState) seal() { + if ps.timeout != nil { + ps.timeout.Stop() + } + ps.sealed = true + ps.logger.Debug("publisher state sealing", "peer", ps.pid, "height", ps.bs.height, "broken", ps.broken) + ps.pool.publisherStateSealCh <- ps +} + +// ----------------------- +type Message struct { + blockChainMessage BlockchainMessage + peerId p2p.ID +} + +type decayedPeer struct { + peerId p2p.ID + fromHeight int64 +} + +type subFromStoreMsg struct { + src p2p.Peer + height int64 +} + +func makeBlockID(block *types.Block) *types.BlockID { + blockParts := block.MakePartSet(types.BlockPartSizeBytes) + blockPartsHeader := blockParts.Header() + return &types.BlockID{Hash: block.Hash(), PartsHeader: blockPartsHeader} +} + +type peerState interface { + SetHeight(height int64) +} diff --git a/blockchain/hot/pool_test.go b/blockchain/hot/pool_test.go new file mode 100644 index 000000000..a648b4f3d --- /dev/null +++ b/blockchain/hot/pool_test.go @@ -0,0 +1,604 @@ +package hot + +import ( + "os" + "sort" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/blockchain" + cfg "github.com/tendermint/tendermint/config" + cmn "github.com/tendermint/tendermint/libs/common" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p/mock" + "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" +) + +var ( + config *cfg.Config +) + +func TestBlockPoolHotSyncBasic(t *testing.T) { + // prepare + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + messageChan := poolPair.messageQueue + pool.Start() + defer pool.Stop() + testPeer := mock.NewPeer(nil) + pool.AddPeer(testPeer) + + // consume subscribe message + <-messageChan + + //handle subscribe message + subscriber := mock.NewPeer(nil) + totalTestBlock := int64(10) + pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) + + for i := int64(0); i < totalTestBlock; i++ { + height := pool.currentHeight() + block, commit, _ := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) + expectedBlockMess1 := blockCommitResponseMessage{Block: block} + expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} + pool.handleBlockCommit(block, commit, testPeer) + var receive1, receive2 bool + for m := range messageChan { + if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { + if !receive1 { + assert.Equal(t, *bm, expectedBlockMess1) + receive1 = true + } else if !receive2 { + assert.Equal(t, *bm, expectedBlockMess2) + receive2 = true + } + if receive1 && receive2 { + break + } + } + } + for { + if height+1 != pool.currentHeight() { + time.Sleep(10 * time.Millisecond) + } else { + break + } + } + } + assert.Equal(t, int64(pool.blocksSynced), totalTestBlock) +} + +func TestBlockPoolHotSyncTimeout(t *testing.T) { + // prepare + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + messageChan := poolPair.messageQueue + pool.Start() + defer pool.Stop() + testPeer := mock.NewPeer(nil) + pool.AddPeer(testPeer) + + // send subscribe message for several times + expectedMessages := make([]BlockchainMessage, 0, 1+2*maxSubscribeForesight) + expectedMessages = append(expectedMessages, &blockSubscribeMessage{FromHeight: initBlockHeight + 1, ToHeight: initBlockHeight + maxSubscribeForesight}) + for j := 0; j < 3; j++ { + for i := int64(0); i < maxSubscribeForesight; i++ { + expectedMessages = append(expectedMessages, &blockUnSubscribeMessage{Height: initBlockHeight + i + 1}) + expectedMessages = append(expectedMessages, &blockSubscribeMessage{FromHeight: initBlockHeight + i + 1, ToHeight: initBlockHeight + i + 1}) + } + } + for _, expect := range expectedMessages { + m := <-messageChan + assert.Equal(t, m.blockChainMessage, expect) + } +} + +func TestBlockPoolHotSyncSubscribePastBlock(t *testing.T) { + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(100) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + messageChan := poolPair.messageQueue + pool.Start() + defer pool.Stop() + + subscriber := mock.NewPeer(nil) + pool.handleSubscribeBlock(1, initBlockHeight, subscriber) + + expectBlockMessage := make([]blockCommitResponseMessage, initBlockHeight-1) + for i := int64(1); i < int64(initBlockHeight); i++ { + first := pool.store.LoadBlock(i) + second := pool.store.LoadBlock(i + 1) + expectBlockMessage[i-1].Block = first + expectBlockMessage[i-1].Commit = second.LastCommit + } + var index int + for m := range messageChan { + if blockMes, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { + assert.Equal(t, *blockMes, expectBlockMessage[index]) + index++ + if int64(index) >= initBlockHeight-1 { + break + } + } + } +} + +func TestBlockPoolHotSyncSubscribeTooFarBlock(t *testing.T) { + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + messageChan := poolPair.messageQueue + pool.Start() + defer pool.Stop() + + subscriber := mock.NewPeer(nil) + pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+2*maxPublishForesight, subscriber) + for m := range messageChan { + if noBlock, ok := m.blockChainMessage.(*noBlockResponseMessage); ok { + assert.Equal(t, noBlock.Height, initBlockHeight+maxPublishForesight) + return + } + } +} + +func TestBlockPoolHotSyncUnSubscribe(t *testing.T) { + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + pool.Start() + defer pool.Stop() + + subscriber := mock.NewPeer(nil) + totalTestBlock := int64(10) + pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) + for i := initBlockHeight + 1; i <= initBlockHeight+totalTestBlock; i++ { + pool.handleUnSubscribeBlock(i, subscriber) + assert.Zero(t, len(pool.subscriberPeerSets[i])) + } +} + +func TestBlockPoolHotSyncRemovePeer(t *testing.T) { + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + messageChan := poolPair.messageQueue + pool.Start() + defer pool.Stop() + + testPeer := mock.NewPeer(nil) + pool.AddPeer(testPeer) + // wait for peer have a try + time.Sleep(2 * tryRepairInterval) + pool.RemovePeer(testPeer) + // drain subscribe message + <-messageChan + for i := int64(0); i < maxSubscribeForesight; i++ { + m := <-messageChan + noBlock, ok := m.blockChainMessage.(*blockUnSubscribeMessage) + assert.True(t, ok) + assert.Equal(t, noBlock.Height, initBlockHeight+i+1) + } +} + +func TestBlockPoolConsensusSyncBasic(t *testing.T) { + eventBus := types.NewEventBus() + err := eventBus.Start() + require.NoError(t, err) + defer eventBus.Stop() + + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + pool.st = Consensus + pool.setEventBus(eventBus) + messageChan := poolPair.messageQueue + pool.Start() + defer pool.Stop() + //handle subscribe message + subscriber := mock.NewPeer(nil) + totalTestBlock := int64(10) + pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) + + for i := int64(0); i < totalTestBlock; i++ { + height := pool.currentHeight() + block, commit, vs := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) + expectedBlockMess1 := blockCommitResponseMessage{Block: block} + //generate hash + commit.Hash() + expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} + err := eventBus.PublishEventCompleteProposal(types.EventDataCompleteProposal{Height: block.Height, Block: *block}) + assert.NoError(t, err) + err = eventBus.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{Height: block.Height, Votes: *vs}) + assert.NoError(t, err) + pool.applyBlock(&blockState{ + block: block, + commit: commit, + blockId: makeBlockID(block), + }) + var receive1, receive2 bool + for m := range messageChan { + if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { + if !receive1 { + assert.Equal(t, *bm, expectedBlockMess1) + receive1 = true + } else if !receive2 { + assert.Equal(t, *bm, expectedBlockMess2) + receive2 = true + } + if receive1 && receive2 { + break + } + } + } + for { + if height+1 != pool.currentHeight() { + time.Sleep(10 * time.Millisecond) + } else { + break + } + } + } +} + +func TestBlockPoolSubscribeFromCache(t *testing.T) { + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + messageChan := poolPair.messageQueue + pool.Start() + defer pool.Stop() + testPeer := mock.NewPeer(nil) + pool.AddPeer(testPeer) + + // consume subscribe message + <-messageChan + + totalTestBlock := int64(10) + + expectedBlockMess := make([]blockCommitResponseMessage, 0, totalTestBlock) + for i := int64(0); i < totalTestBlock; i++ { + height := pool.currentHeight() + block, commit, _ := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) + expectedBlockMess = append(expectedBlockMess, blockCommitResponseMessage{Block: block, Commit: commit}) + pool.handleBlockCommit(block, commit, testPeer) + + for { + if height+1 != pool.currentHeight() { + time.Sleep(10 * time.Millisecond) + } else { + break + } + } + } + //handle subscribe message + subscriber := mock.NewPeer(nil) + pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) + for i := int64(0); i < totalTestBlock; i++ { + for m := range messageChan { + if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { + assert.Equal(t, *bm, expectedBlockMess[i]) + break + } + } + } +} + +func TestBlockPoolSwitch(t *testing.T) { + config = cfg.ResetTestRoot(t.Name()) + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + initBlockHeight := int64(10) + poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) + + pool := poolPair.pool + messageChan := poolPair.messageQueue + pool.st = Mute + pool.Start() + defer pool.Stop() + testPeer := mock.NewPeer(nil) + pool.AddPeer(testPeer) + + time.Sleep(10 * time.Millisecond) + pool.SwitchToHotSync(pool.state, 100) + // consume subscribe message + <-messageChan + + //handle subscribe message + subscriber := mock.NewPeer(nil) + totalTestBlock := int64(10) + pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) + + for i := int64(0); i < totalTestBlock; i++ { + height := pool.currentHeight() + block, commit, _ := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) + expectedBlockMess1 := blockCommitResponseMessage{Block: block} + expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} + pool.handleBlockCommit(block, commit, testPeer) + var receive1, receive2 bool + for m := range messageChan { + if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { + if !receive1 { + assert.Equal(t, *bm, expectedBlockMess1) + receive1 = true + } else if !receive2 { + assert.Equal(t, *bm, expectedBlockMess2) + receive2 = true + } + if receive1 && receive2 { + break + } + } + } + for { + if height+1 != pool.currentHeight() { + time.Sleep(10 * time.Millisecond) + } else { + break + } + } + } + assert.Equal(t, int64(pool.blocksSynced), totalTestBlock+100) + eventBus := types.NewEventBus() + err := eventBus.Start() + require.NoError(t, err) + defer eventBus.Stop() + pool.setEventBus(eventBus) + + pool.SwitchToConsensusSync(nil) + time.Sleep(10 * time.Millisecond) + subscriber = mock.NewPeer(nil) + totalTestBlock = int64(10) + pool.handleSubscribeBlock(pool.currentHeight()+1, pool.currentHeight()+totalTestBlock, subscriber) + + for i := int64(0); i < totalTestBlock; i++ { + height := pool.currentHeight() + block, commit, vs := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) + expectedBlockMess1 := blockCommitResponseMessage{Block: block} + //generate hash + commit.Hash() + expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} + err := eventBus.PublishEventCompleteProposal(types.EventDataCompleteProposal{Height: block.Height, Block: *block}) + assert.NoError(t, err) + err = eventBus.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{Height: block.Height, Votes: *vs}) + assert.NoError(t, err) + pool.applyBlock(&blockState{ + block: block, + commit: commit, + blockId: makeBlockID(block), + }) + var receive1, receive2 bool + for m := range messageChan { + if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { + if !receive1 { + eq := assert.Equal(t, *bm, expectedBlockMess1) + receive1 = true + if !eq { + return + } + } else if !receive2 { + eq := assert.Equal(t, *bm, expectedBlockMess2) + receive2 = true + receive1 = true + if !eq { + return + } + } + if receive1 && receive2 { + break + } + } + } + for { + if height+1 != pool.currentHeight() { + time.Sleep(10 * time.Millisecond) + } else { + break + } + } + } + +} + +func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.GenesisDoc, []types.PrivValidator) { + validators := make([]types.GenesisValidator, numValidators) + privValidators := make([]types.PrivValidator, numValidators) + for i := 0; i < numValidators; i++ { + val, privVal := types.RandValidator(randPower, minPower) + validators[i] = types.GenesisValidator{ + PubKey: val.PubKey, + Power: val.VotingPower, + } + privValidators[i] = privVal + } + sort.Sort(types.PrivValidatorsByAddress(privValidators)) + + return &types.GenesisDoc{ + GenesisTime: tmtime.Now(), + ChainID: config.ChainID(), + Validators: validators, + }, privValidators +} + +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < 10; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) + return block +} + +func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { + addr := privVal.GetPubKey().Address() + idx, _ := valset.GetByAddress(addr) + vote := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: header.Height, + Round: 1, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + + privVal.SignVote(header.ChainID, vote) + + return vote +} + +func newBlockchainPoolPair(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) BlockPoolPair { + if len(privVals) != 1 { + panic("only support one validator") + } + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(cc) + err := proxyApp.Start() + if err != nil { + panic(cmn.ErrorWrap(err, "error start app")) + } + blockDB := dbm.NewMemDB() + stateDB := dbm.NewMemDB() + blockStore := blockchain.NewBlockStore(blockDB) + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + if err != nil { + panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + } + + // Make the BlockPool itself. + // NOTE we have to create and commit the blocks first because + // pool.height is determined from the store. + blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), proxyApp.Consensus(), + sm.MockMempool{}, sm.MockEvidencePool{}, true) + // let's add some blocks in + for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { + lastCommit := types.NewCommit(types.BlockID{}, nil) + if blockHeight > 1 { + lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) + lastBlock := blockStore.LoadBlock(blockHeight - 1) + + vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + } + + thisBlock := makeBlock(blockHeight, state, lastCommit) + + thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) + blockID := types.BlockID{thisBlock.Hash(), thisParts.Header()} + + state, err = blockExec.ApplyBlock(state, blockID, thisBlock) + if err != nil { + panic(cmn.ErrorWrap(err, "error apply block")) + } + + blockStore.SaveBlock(thisBlock, thisParts, lastCommit) + } + messagesCh := make(chan Message, messageQueueSize) + bcPool := NewBlockPool(blockStore, blockExec, state.Copy(), messagesCh, Hot, 2*time.Second) + bcPool.SetLogger(logger.With("module", "blockpool")) + + return BlockPoolPair{bcPool, proxyApp, messagesCh, privVals} +} + +type BlockPoolPair struct { + pool *BlockPool + app proxy.AppConns + messageQueue chan Message + privVals []types.PrivValidator +} + +func nextBlock(state sm.State, blockStore *blockchain.BlockStore, blockExec *sm.BlockExecutor, pri types.PrivValidator) (*types.Block, *types.Commit, *types.VoteSet) { + height := blockStore.Height() + lastBlockMeta := blockStore.LoadBlockMeta(height) + lastBlock := blockStore.LoadBlock(height) + vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, pri).CommitSig() + lastCommit := types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + thisBlock := makeBlock(height+1, state, lastCommit) + + thisBlockId := makeBlockID(thisBlock) + thisVote := makeVote(&thisBlock.Header, *thisBlockId, state.Validators, pri) + thisCommitSig := thisVote.CommitSig() + thisCommit := types.NewCommit(*makeBlockID(thisBlock), []*types.CommitSig{thisCommitSig}) + thisVoteSet := types.NewVoteSet(thisBlock.ChainID, thisBlock.Height, 1, types.PrecommitType, state.Validators) + thisVoteSet.AddVote(thisVote) + return thisBlock, thisCommit, thisVoteSet +} + +type testApp struct { + abci.BaseApplication +} + +var _ abci.Application = (*testApp)(nil) + +func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) { + return abci.ResponseInfo{} +} + +func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { + return abci.ResponseBeginBlock{} +} + +func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { + return abci.ResponseEndBlock{} +} + +func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { + return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} +} + +func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { + return abci.ResponseCheckTx{} +} + +func (app *testApp) Commit() abci.ResponseCommit { + return abci.ResponseCommit{} +} + +func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { + return +} diff --git a/blockchain/hot/reactor.go b/blockchain/hot/reactor.go new file mode 100644 index 000000000..6643e0ee0 --- /dev/null +++ b/blockchain/hot/reactor.go @@ -0,0 +1,365 @@ +package hot + +import ( + "errors" + "fmt" + "reflect" + "time" + + "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/blockchain" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" +) + +const ( + // HotBlockchainChannel is a channel for blocks/commits and status updates under hot sync mode. + HotBlockchainChannel = byte(0x41) + + messageQueueSize = 1000 + + switchToConsensusIntervalSeconds = 1 + + maxSubscribeBlocks = 50 + + // NOTE: keep up to date with blockCommitResponseMessage + blockCommitResponseMessagePrefixSize = 4 + blockCommitMessageFieldKeySize = 2 + // TODO, the size of commit is dynamic, assume no more than 1M. + commitSizeBytes = 1024 * 1024 + maxMsgSize = types.MaxBlockSizeBytes + + blockCommitResponseMessagePrefixSize + + blockCommitMessageFieldKeySize + + commitSizeBytes +) + +type consensusReactor interface { + // for when we switch from hotBlockchain reactor and hot sync to + // the consensus machine + SwitchToConsensus(sm.State, int) +} + +// BlockchainReactor handles low latency catchup when there is small lag. +type BlockchainReactor struct { + p2p.BaseReactor + + pool *BlockPool + privValidator types.PrivValidator + + messagesCh <-chan Message +} + +type BlockChainOption func(*BlockchainReactor) + +// NewBlockChainReactor returns new reactor instance. +func NewBlockChainReactor(state sm.State, blockExec *sm.BlockExecutor, store *blockchain.BlockStore, hotSync, fastSync bool, blockTimeout time.Duration, options ...BlockChainOption) *BlockchainReactor { + + if state.LastBlockHeight != store.Height() { + panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, + store.Height())) + } + + messagesCh := make(chan Message, messageQueueSize) + var st SyncPattern + if fastSync { + st = Mute + } else if hotSync { + st = Hot + } else { + st = Consensus + } + pool := NewBlockPool(store, blockExec, state, messagesCh, st, blockTimeout) + + hbcR := &BlockchainReactor{ + messagesCh: messagesCh, + pool: pool, + } + + for _, option := range options { + option(hbcR) + } + + hbcR.BaseReactor = *p2p.NewBaseReactor("HotSyncBlockChainReactor", hbcR) + return hbcR +} + +// WithMetrics sets the metrics. +func WithMetrics(metrics *Metrics) BlockChainOption { + return func(hbcR *BlockchainReactor) { hbcR.pool.setMetrics(metrics) } +} + +// WithMetrics sets the metrics. +func WithEventBus(eventBs *types.EventBus) BlockChainOption { + return func(hbcR *BlockchainReactor) { hbcR.pool.setEventBus(eventBs) } +} + +func (hbcR *BlockchainReactor) SetPrivValidator(priv types.PrivValidator) { + hbcR.privValidator = priv +} + +// SetLogger implements cmn.Service by setting the logger on reactor and pool. +func (hbcR *BlockchainReactor) SetLogger(l log.Logger) { + hbcR.BaseService.Logger = l + hbcR.pool.SetLogger(l) +} + +// OnStart implements cmn.Service. +func (hbcR *BlockchainReactor) OnStart() error { + err := hbcR.pool.Start() + if err != nil { + return err + } + go hbcR.poolRoutine() + go hbcR.switchRoutine() + return nil +} + +// OnStop implements cmn.Service. +func (hbcR *BlockchainReactor) OnStop() { + hbcR.pool.Stop() +} + +// GetChannels implements Reactor +func (hbcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { + return []*p2p.ChannelDescriptor{ + { + ID: HotBlockchainChannel, + Priority: 10, + SendQueueCapacity: 1000, + RecvBufferCapacity: 50 * 4096, + RecvMessageCapacity: maxMsgSize, + }, + } +} + +func (hbcR *BlockchainReactor) AddPeer(peer p2p.Peer) { + hbcR.pool.AddPeer(peer) +} + +func (hbcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { + hbcR.pool.RemovePeer(peer) +} + +// Receive implements Reactor by handling 5 types of messages (look below). +func (hbcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { + msg, err := decodeMsg(msgBytes) + if err != nil { + hbcR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + hbcR.Switch.StopPeerForError(src, err) + return + } + + if err = msg.ValidateBasic(); err != nil { + hbcR.Logger.Error("Peer sent us invalid msg", "peer", src, "msg", msg, "err", err) + hbcR.Switch.StopPeerForError(src, err) + return + } + switch msg := msg.(type) { + case *blockSubscribeMessage: + hbcR.Logger.Debug("receive blockSubscribeMessage from peer", "peer", src.ID(), "from_height", msg.FromHeight, "to_height", msg.ToHeight) + hbcR.pool.handleSubscribeBlock(msg.FromHeight, msg.ToHeight, src) + case *blockCommitResponseMessage: + hbcR.Logger.Debug("receive blockCommitResponseMessage from peer", "peer", src.ID(), "height", msg.Height()) + hbcR.pool.handleBlockCommit(msg.Block, msg.Commit, src) + case *noBlockResponseMessage: + hbcR.Logger.Debug("receive noBlockResponseMessage from peer", "peer", src.ID(), "height", msg.Height) + hbcR.pool.handleNoBlock(msg.Height, src) + case *blockUnSubscribeMessage: + hbcR.Logger.Debug("receive blockUnSubscribeMessage from peer", "peer", src.ID(), "height", msg.Height) + hbcR.pool.handleUnSubscribeBlock(msg.Height, src) + default: + hbcR.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) + } +} + +func (hbcR *BlockchainReactor) SetSwitch(sw *p2p.Switch) { + hbcR.Switch = sw + hbcR.pool.candidatePool.swh = sw +} + +func (hbcR *BlockchainReactor) SwitchToHotSync(state sm.State, blockSynced int32) { + hbcR.pool.SwitchToHotSync(state, blockSynced) +} + +func (hbcR *BlockchainReactor) SwitchToConsensusSync(state sm.State) { + hbcR.pool.SwitchToConsensusSync(&state) +} + +func (hbcR *BlockchainReactor) switchRoutine() { + switchToConsensusTicker := time.NewTicker(switchToConsensusIntervalSeconds * time.Second) + defer switchToConsensusTicker.Stop() + for { + select { + case <-hbcR.Quit(): + return + case <-hbcR.pool.Quit(): + return + case <-switchToConsensusTicker.C: + if hbcR.pool.getSyncPattern() == Hot && hbcR.privValidator != nil && hbcR.pool.state.Validators.HasAddress(hbcR.privValidator.GetAddress()) { + hbcR.Logger.Info("hot sync switching to consensus sync") + conR, ok := hbcR.Switch.Reactor("CONSENSUS").(consensusReactor) + if ok { + hbcR.pool.SwitchToConsensusSync(nil) + conR.SwitchToConsensus(hbcR.pool.state, int(hbcR.pool.getBlockSynced())) + return + } else { + // should only happen during testing + } + } + } + } +} + +func (hbcR *BlockchainReactor) poolRoutine() { + for { + select { + case <-hbcR.Quit(): + return + case <-hbcR.pool.Quit(): + return + case message := <-hbcR.messagesCh: + peer := hbcR.Switch.Peers().Get(message.peerId) + if peer == nil { + continue + } + msgBytes := cdc.MustMarshalBinaryBare(message.blockChainMessage) + hbcR.Logger.Debug(fmt.Sprintf("send message %s", message.blockChainMessage.String()), "peer", peer.ID()) + queued := peer.TrySend(HotBlockchainChannel, msgBytes) + if !queued { + hbcR.Logger.Debug("Send queue is full or no hot sync channel, drop blockChainMessage", "peer", peer.ID()) + } + } + } +} + +// BlockchainMessage is a generic message for this reactor. +type BlockchainMessage interface { + ValidateBasic() error + String() string +} + +//------------------------------------- + +// Subscribe block at specified height range +type blockSubscribeMessage struct { + FromHeight int64 + ToHeight int64 +} + +func (m *blockSubscribeMessage) ValidateBasic() error { + if m.FromHeight < 0 { + return errors.New("Negative Height") + } + if m.ToHeight < m.FromHeight { + return errors.New("Height is greater than ToHeight") + } + if m.ToHeight-m.FromHeight > maxSubscribeBlocks { + return errors.New("Subscribe too many blocks") + } + return nil +} + +func (m *blockSubscribeMessage) String() string { + return fmt.Sprintf("[blockSubscribeMessage from %v, to %v]", m.FromHeight, m.ToHeight) +} + +//------------------------------------- + +// UnSubscribe block at specified height +// Why not in range? +// Because of peer pick strategy, will not subscribe continuous blocks from peer that considered as unknown or bad. +// And because of reschedule strategy, the previous continuous subscription become discontinuous. +type blockUnSubscribeMessage struct { + Height int64 +} + +func (m *blockUnSubscribeMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("Negative Height") + } + return nil +} + +func (m *blockUnSubscribeMessage) String() string { + return fmt.Sprintf("[blockUnSubscribeMessage at %v]", m.Height) +} + +//------------------------------------- +type noBlockResponseMessage struct { + Height int64 +} + +func (m *noBlockResponseMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("Negative Height") + } + return nil +} + +func (m *noBlockResponseMessage) String() string { + return fmt.Sprintf("[noBlockResponseMessage %v]", m.Height) +} + +//------------------------------------- + +type blockCommitResponseMessage struct { + Block *types.Block + Commit *types.Commit +} + +func (m *blockCommitResponseMessage) Height() int64 { + // have checked, block and commit can't both be nil + if m.Block != nil { + return m.Block.Height + } else { + return m.Commit.Height() + } +} + +func (m *blockCommitResponseMessage) ValidateBasic() error { + if m.Block == nil && m.Commit == nil { + return errors.New("Both Commit and Block field are missing") + } + + if m.Commit != nil && m.Block != nil { + blockId := makeBlockID(m.Block) + if !blockId.Equals(m.Commit.BlockID) { + return errors.New("BlockID mismatch") + } + } + if m.Commit != nil { + if err := m.Commit.ValidateBasic(); err != nil { + return err + } + } + if m.Block != nil { + if err := m.Block.ValidateBasic(); err != nil { + return err + } + } + return nil +} + +func (m *blockCommitResponseMessage) String() string { + return fmt.Sprintf("[blockCommitResponseMessage %v]", m.Height()) +} + +//------------------------------------- + +func RegisterHotBlockchainMessages(cdc *amino.Codec) { + cdc.RegisterInterface((*BlockchainMessage)(nil), nil) + cdc.RegisterConcrete(&blockSubscribeMessage{}, "tendermint/blockchain/hotBlockSubscribe", nil) + cdc.RegisterConcrete(&blockUnSubscribeMessage{}, "tendermint/blockchain/hotBlockUnSubscribe", nil) + cdc.RegisterConcrete(&blockCommitResponseMessage{}, "tendermint/blockchain/hotBlockCommitResponse", nil) + cdc.RegisterConcrete(&noBlockResponseMessage{}, "tendermint/blockchain/hotNoBlockResponse", nil) +} + +func decodeMsg(bz []byte) (msg BlockchainMessage, err error) { + if len(bz) > maxMsgSize { + return msg, fmt.Errorf("Msg exceeds max size (%d > %d)", len(bz), maxMsgSize) + } + err = cdc.UnmarshalBinaryBare(bz, &msg) + return +} diff --git a/blockchain/hot/reactor_test.go b/blockchain/hot/reactor_test.go new file mode 100644 index 000000000..2d9f58e9f --- /dev/null +++ b/blockchain/hot/reactor_test.go @@ -0,0 +1,260 @@ +package hot + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/tendermint/blockchain" + cfg "github.com/tendermint/tendermint/config" + cmn "github.com/tendermint/tendermint/libs/common" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/types" +) + +func TestHotSyncReactorBasic(t *testing.T) { + config = cfg.ResetTestRoot("blockchain_reactor_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + + maxBlockHeight := int64(65) + + reactorPairs := make([]BlockChainReactorPair, 2) + + eventBus1 := types.NewEventBus() + err := eventBus1.Start() + defer eventBus1.Stop() + assert.NoError(t, err) + eventBus2 := types.NewEventBus() + err = eventBus2.Start() + defer eventBus2.Stop() + assert.NoError(t, err) + reactorPairs[0] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, maxBlockHeight, eventBus1, true, false) + reactorPairs[1] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, 0, eventBus2, true, false) + + p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { + s.AddReactor("hot", reactorPairs[i].reactor) + return s + + }, p2p.Connect2Switches) + + defer func() { + for _, r := range reactorPairs { + r.reactor.Stop() + r.app.Stop() + } + }() + + tests := []struct { + height int64 + existent bool + }{ + {maxBlockHeight + 2, false}, + {10, true}, + {1, true}, + {100, false}, + } + + for { + if reactorPairs[1].reactor.pool.currentHeight() == maxBlockHeight-1 { + break + } + time.Sleep(10 * time.Millisecond) + } + + for _, tt := range tests { + block := reactorPairs[1].reactor.pool.store.LoadBlock(tt.height) + if tt.existent { + assert.True(t, block != nil) + } else { + assert.True(t, block == nil) + } + } +} + +func TestHotSyncReactorSwitch(t *testing.T) { + config = cfg.ResetTestRoot("blockchain_reactor_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + + maxBlockHeight := int64(65) + + reactorPairs := make([]BlockChainReactorPair, 2) + + eventBus0 := types.NewEventBus() + err := eventBus0.Start() + defer eventBus0.Stop() + assert.NoError(t, err) + eventBus1 := types.NewEventBus() + err = eventBus1.Start() + defer eventBus1.Stop() + assert.NoError(t, err) + reactorPairs[0] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, maxBlockHeight, eventBus0, true, false) + reactorPairs[1] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, 0, eventBus1, true, true) + + p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { + s.AddReactor("hot", reactorPairs[i].reactor) + return s + + }, p2p.Connect2Switches) + + defer func() { + for _, r := range reactorPairs { + r.reactor.Stop() + r.app.Stop() + } + }() + + time.Sleep(100 * time.Millisecond) + assert.Equal(t, reactorPairs[1].reactor.pool.currentHeight(), int64(0)) + + for i := int64(0); i < 10; i++ { + first := reactorPairs[0].reactor.pool.store.LoadBlock(i + 1) + bId := makeBlockID(first) + second := reactorPairs[0].reactor.pool.store.LoadBlock(i + 2) + reactorPairs[1].reactor.pool.applyBlock(&blockState{block: first, commit: second.LastCommit, blockId: bId}) + } + + reactorPairs[1].reactor.SwitchToHotSync(reactorPairs[1].reactor.pool.state, 10) + + tests := []struct { + height int64 + existent bool + }{ + {maxBlockHeight + 2, false}, + {10, true}, + {1, true}, + {100, false}, + } + + for { + if reactorPairs[1].reactor.pool.currentHeight() == maxBlockHeight-1 { + break + } + time.Sleep(10 * time.Millisecond) + } + + for _, tt := range tests { + block := reactorPairs[1].reactor.pool.store.LoadBlock(tt.height) + if tt.existent { + assert.True(t, block != nil) + } else { + assert.True(t, block == nil) + } + } + + reactorPairs[0].reactor.SwitchToConsensusSync(reactorPairs[0].reactor.pool.state) + + pool := reactorPairs[0].reactor.pool + consensusHeight := int64(100) + for i := int64(0); i < consensusHeight; i++ { + height := pool.currentHeight() + block, commit, vs := nextBlock(pool.state, pool.store, pool.blockExec, reactorPairs[0].privVals[0]) + + err := eventBus0.PublishEventCompleteProposal(types.EventDataCompleteProposal{Height: block.Height, Block: *block}) + assert.NoError(t, err) + err = eventBus0.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{Height: block.Height, Votes: *vs}) + assert.NoError(t, err) + pool.applyBlock(&blockState{ + block: block, + commit: commit, + blockId: makeBlockID(block), + }) + for { + if height+1 != pool.currentHeight() { + time.Sleep(10 * time.Millisecond) + } else { + break + } + } + } + + tests2 := []struct { + height int64 + existent bool + }{ + {reactorPairs[1].reactor.pool.currentHeight(), true}, + {122, true}, + {50, true}, + {200, false}, + } + + for { + if reactorPairs[1].reactor.pool.currentHeight() == maxBlockHeight+consensusHeight { + break + } + time.Sleep(10 * time.Millisecond) + } + + for _, tt := range tests2 { + block := reactorPairs[1].reactor.pool.store.LoadBlock(tt.height) + if tt.existent { + assert.True(t, block != nil) + } else { + assert.True(t, block == nil) + } + } +} + +type BlockChainReactorPair struct { + reactor *BlockchainReactor + app proxy.AppConns + privVals []types.PrivValidator +} + +func newBlockchainReactorPair(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64, eventBus *types.EventBus, hotsync, fastSync bool) BlockChainReactorPair { + if len(privVals) != 1 { + panic("only support one validator") + } + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(cc) + err := proxyApp.Start() + if err != nil { + panic(cmn.ErrorWrap(err, "error start app")) + } + blockDB := dbm.NewMemDB() + stateDB := dbm.NewMemDB() + blockStore := blockchain.NewBlockStore(blockDB) + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + if err != nil { + panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + } + + // Make the BlockPool itself. + // NOTE we have to create and commit the blocks first because + // pool.height is determined from the store. + blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), proxyApp.Consensus(), + sm.MockMempool{}, sm.MockEvidencePool{}, true) + // let's add some blocks in + for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { + lastCommit := types.NewCommit(types.BlockID{}, nil) + if blockHeight > 1 { + lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) + lastBlock := blockStore.LoadBlock(blockHeight - 1) + + vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + } + + thisBlock := makeBlock(blockHeight, state, lastCommit) + + thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) + blockID := types.BlockID{thisBlock.Hash(), thisParts.Header()} + + state, err = blockExec.ApplyBlock(state, blockID, thisBlock) + if err != nil { + panic(cmn.ErrorWrap(err, "error apply block")) + } + blockStore.SaveBlock(thisBlock, thisParts, lastCommit) + } + bcReactor := NewBlockChainReactor(state.Copy(), blockExec, blockStore, hotsync, fastSync, 2*time.Second, WithEventBus(eventBus)) + bcReactor.SetLogger(logger.With("module", "hotsync")) + return BlockChainReactorPair{bcReactor, proxyApp, privVals} +} diff --git a/blockchain/hot/wire.go b/blockchain/hot/wire.go new file mode 100644 index 000000000..f90343024 --- /dev/null +++ b/blockchain/hot/wire.go @@ -0,0 +1,13 @@ +package hot + +import ( + "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/types" +) + +var cdc = amino.NewCodec() + +func init() { + RegisterHotBlockchainMessages(cdc) + types.RegisterBlockAmino(cdc) +} diff --git a/blockchain/pool.go b/blockchain/pool.go index f641ec623..29516a1b6 100644 --- a/blockchain/pool.go +++ b/blockchain/pool.go @@ -450,7 +450,7 @@ func (peer *bpPeer) setLogger(l log.Logger) { } func (peer *bpPeer) resetMonitor() { - peer.recvMonitor = flow.New(time.Second, time.Second * types.MonitorWindowInSeconds) + peer.recvMonitor = flow.New(time.Second, time.Second*types.MonitorWindowInSeconds) initialValue := float64(minRecvRate) * math.E peer.recvMonitor.SetREMA(initialValue) } diff --git a/blockchain/reactor.go b/blockchain/reactor.go index 1f39c1afb..4b44f745c 100644 --- a/blockchain/reactor.go +++ b/blockchain/reactor.go @@ -6,8 +6,7 @@ import ( "reflect" "time" - amino "github.com/tendermint/go-amino" - + "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" @@ -15,7 +14,7 @@ import ( ) const ( - // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) + // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) under fast sync mode. BlockchainChannel = byte(0x40) trySyncIntervalMS = 10 @@ -43,6 +42,11 @@ type consensusReactor interface { SwitchToConsensus(sm.State, int) } +type hotsyncReactor interface { + SwitchToHotSync(sm.State, int32) + SwitchToConsensusSync(sm.State) +} + type peerError struct { err error peerID p2p.ID @@ -59,10 +63,12 @@ type BlockchainReactor struct { // immutable initialState sm.State - blockExec *sm.BlockExecutor - store *BlockStore - pool *BlockPool - fastSync bool + blockExec *sm.BlockExecutor + store *BlockStore + pool *BlockPool + fastSync bool + hotSyncReactor bool + hotsync bool requestsCh <-chan BlockRequest errorsCh <-chan peerError @@ -70,7 +76,7 @@ type BlockchainReactor struct { // NewBlockchainReactor returns new reactor instance. func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *BlockStore, - fastSync bool) *BlockchainReactor { + fastSync, hotSyncReactor, hotSync bool) *BlockchainReactor { if state.LastBlockHeight != store.Height() { panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, @@ -89,13 +95,15 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *Bl ) bcR := &BlockchainReactor{ - initialState: state, - blockExec: blockExec, - store: store, - pool: pool, - fastSync: fastSync, - requestsCh: requestsCh, - errorsCh: errorsCh, + initialState: state, + blockExec: blockExec, + store: store, + pool: pool, + fastSync: fastSync, + requestsCh: requestsCh, + errorsCh: errorsCh, + hotSyncReactor: hotSyncReactor, + hotsync: hotSync, } bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) return bcR @@ -293,13 +301,33 @@ FOR_LOOP: // we need make sure blockstore has a block because when switch to consensus, it will verify the commit between block and state // refer to `cs.blockStore.LoadSeenCommit(state.LastBlockHeight)` if bcR.pool.IsCaughtUp() && (height == bcR.pool.initHeight || blocksSynced > 0) { - bcR.Logger.Info("Time to switch to consensus reactor!", "height", height) bcR.pool.Stop() - conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) - if ok { - conR.SwitchToConsensus(state, blocksSynced) + if bcR.hotSyncReactor && bcR.hotsync { + bcR.Logger.Info("Time to switch to hot sync reactor!", "height", height) + hotR, ok := bcR.Switch.Reactor("HOT").(hotsyncReactor) + if ok { + hotR.SwitchToHotSync(state, int32(blocksSynced)) + } else { + // should only happen during testing + } + } else { - // should only happen during testing + if bcR.hotSyncReactor { + bcR.Logger.Info("Time to switch hot sync reactor to consensus sync pattern!", "height", height) + hotR, ok := bcR.Switch.Reactor("HOT").(hotsyncReactor) + if ok { + hotR.SwitchToConsensusSync(state) + } else { + // should only happen during testing + } + } + bcR.Logger.Info("Time to switch to consensus reactor!", "height", height) + conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) + if ok { + conR.SwitchToConsensus(state, blocksSynced) + } else { + // should only happen during testing + } } break FOR_LOOP diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index a7be6367d..6c46a4a38 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -118,7 +118,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals blockStore.SaveBlock(thisBlock, thisParts, lastCommit) } - bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync, false, false) bcReactor.SetLogger(logger.With("module", "blockchain")) return BlockchainReactorPair{bcReactor, proxyApp} @@ -255,21 +255,6 @@ func TestBadBlockStopsPeer(t *testing.T) { assert.True(t, lastReactorPair.reactor.Switch.Peers().Size() < len(reactorPairs)-1) } -//---------------------------------------------- -// utility funcs - -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < 10; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) - return block -} - type testApp struct { abci.BaseApplication } diff --git a/blockchain/store_test.go b/blockchain/store_test.go index bd30bc6d2..28ab6051a 100644 --- a/blockchain/store_test.go +++ b/blockchain/store_test.go @@ -45,6 +45,21 @@ func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFu return state, NewBlockStore(blockDB), func() { os.RemoveAll(config.RootDir) } } +//---------------------------------------------- +// utility funcs + +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < 10; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) + return block +} + func TestLoadBlockStoreStateJSON(t *testing.T) { db := db.NewMemDB() diff --git a/config/config.go b/config/config.go index b83b0492d..a3b91b302 100644 --- a/config/config.go +++ b/config/config.go @@ -164,6 +164,18 @@ type BaseConfig struct { // and verifying their commits FastSync bool `mapstructure:"fast_sync"` + // it is for fullnode/witness who do not need consensus to sync block. + HotSyncReactor bool `mapstructure:"hot_sync_reactor"` + + // only take effect when HotSyncReactor is true. + // If true, will sync blocks use hot sync protocol + // If false, still use tendermint consensus protocol, but can still handle other peers sync request. + HotSync bool `mapstructure:"hot_sync"` + + // the max wait time for subscribe a block. + // only take effect when hot_sync is true + HotSyncTimeout time.Duration `mapstructure:"hot_sync_timeout"` + // As state sync is an experimental feature, this switch can totally disable it on core network nodes (validator, witness) StateSyncReactor bool `mapstructure:"state_sync_reactor"` @@ -232,6 +244,9 @@ func DefaultBaseConfig() BaseConfig { ProfListenAddress: "", FastSync: true, StateSyncReactor: true, + HotSync: false, + HotSyncReactor: false, + HotSyncTimeout: 3 * time.Second, StateSyncHeight: -1, FilterPeers: false, DBBackend: "leveldb", @@ -293,6 +308,9 @@ func (cfg BaseConfig) ValidateBasic() error { default: return errors.New("unknown log_format (must be 'plain' or 'json')") } + if !cfg.HotSyncReactor && cfg.HotSync { + return errors.New("config hot_sync can't be true while hot_sync_reactor is false") + } return nil } @@ -726,14 +744,15 @@ func (cfg *DBCacheConfig) ToGolevelDBOpt() *optPkg.Options { // MempoolConfig defines the configuration options for the Tendermint mempool type MempoolConfig struct { - RootDir string `mapstructure:"home"` - Recheck bool `mapstructure:"recheck"` - Broadcast bool `mapstructure:"broadcast"` - WalPath string `mapstructure:"wal_dir"` - Size int `mapstructure:"size"` - MaxTxsBytes int64 `mapstructure:"max_txs_bytes"` - CacheSize int `mapstructure:"cache_size"` - OnlyPersistent bool `mapstructure:"only_persistent"` + RootDir string `mapstructure:"home"` + Recheck bool `mapstructure:"recheck"` + Broadcast bool `mapstructure:"broadcast"` + WalPath string `mapstructure:"wal_dir"` + Size int `mapstructure:"size"` + MaxTxsBytes int64 `mapstructure:"max_txs_bytes"` + CacheSize int `mapstructure:"cache_size"` + OnlyToPersistent bool `mapstructure:"only_to_persistent"` + SkipTxFromPersistent bool `mapstructure:"skip_tx_from_persistent"` } // DefaultMempoolConfig returns a default configuration for the Tendermint mempool @@ -744,10 +763,11 @@ func DefaultMempoolConfig() *MempoolConfig { WalPath: "", // Each signature verification takes .5ms, Size reduced until we implement // ABCI Recheck - Size: 5000, - MaxTxsBytes: 1024 * 1024 * 1024, // 1GB - CacheSize: 10000, - OnlyPersistent: false, + Size: 5000, + MaxTxsBytes: 1024 * 1024 * 1024, // 1GB + CacheSize: 10000, + OnlyToPersistent: false, + SkipTxFromPersistent: false, } } diff --git a/config/toml.go b/config/toml.go index a2b55c36d..c60907efa 100644 --- a/config/toml.go +++ b/config/toml.go @@ -81,6 +81,18 @@ moniker = "{{ .BaseConfig.Moniker }}" # and verifying their commits fast_sync = {{ .BaseConfig.FastSync }} +# Only take effect when HotSyncReactor is true. +# If true, will sync blocks use hot sync protocol +# If false, still use tendermint consensus protocol, but can still handle other peers sync request. +hot_sync = {{.BaseConfig.HotSync}} + +# The max wait time for subscribe a block. +# Only take effect when hot_sync is true +hot_sync_timeout = "{{.BaseConfig.HotSyncTimeout}}" + +# It will benefit fullnode and witness who do not need consensus by saving network and cpu resources. +# Recommend the node that is not validator to turn on. +hot_sync_reactor = {{.BaseConfig.HotSyncReactor}} # As state sync is an experimental feature, this switch can totally disable it on core network nodes (validator, witness) state_sync_reactor = {{ .BaseConfig.StateSyncReactor }} @@ -322,7 +334,10 @@ broadcast = {{ .Mempool.Broadcast }} wal_dir = "{{ js .Mempool.WalPath }}" # If set true, will only broadcast transactions to persistent peers. -only_persistent = {{ .Mempool.OnlyPersistent }} +only_to_persistent = {{ .Mempool.OnlyToPersistent }} + +# If set true, only the transaction from none persistent peer will broadcast. +skip_tx_from_persistent = {{ .Mempool.SkipTxFromPersistent }} # Maximum number of transactions in the mempool size = {{ .Mempool.Size }} diff --git a/consensus/reactor.go b/consensus/reactor.go index 035c8f413..61001e938 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -971,6 +971,13 @@ func (ps *PeerState) GetHeight() int64 { return ps.PRS.Height } +// use by hotsync to ensure mempool can broadcast tx to it's peer +func (ps *PeerState) SetHeight(height int64) { + ps.mtx.Lock() + defer ps.mtx.Unlock() + ps.PRS.Height = height +} + // SetHasProposal sets the given proposal as known for the peer. func (ps *PeerState) SetHasProposal(proposal *types.Proposal) { ps.mtx.Lock() diff --git a/consensus/state.go b/consensus/state.go index 3cab6551b..0422890b6 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1661,6 +1661,12 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, blockID, ok := precommits.TwoThirdsMajority() if ok { + cs.eventBus.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{ + Height: cs.Height, + Round: cs.Round, + Step: cs.Step.String(), + Votes: *precommits.Copy(), + }) // Executed as TwoThirdsMajority could be from a higher round cs.enterNewRound(height, vote.Round) cs.enterPrecommit(height, vote.Round) diff --git a/consensus/types/round_state.go b/consensus/types/round_state.go index c4372e201..63e485db2 100644 --- a/consensus/types/round_state.go +++ b/consensus/types/round_state.go @@ -143,6 +143,7 @@ func (rs *RoundState) CompleteProposalEvent() types.EventDataCompleteProposal { Round: rs.Round, Step: rs.Step.String(), BlockID: blockId, + Block: *rs.ProposalBlock, } } diff --git a/mempool/mempool.go b/mempool/mempool.go index 03f086c6f..df7bad082 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -37,6 +37,8 @@ type TxInfo struct { // We don't use p2p.ID here because it's too big. The gain is to store max 2 // bytes with each tx to identify the sender rather than 20 bytes. PeerID uint16 + // whether the tx comes from a persistent peer. + FromPersistent bool } /* @@ -368,7 +370,7 @@ func (mem *Mempool) TxsWaitChan() <-chan struct{} { // It gets called from another goroutine. // CONTRACT: Either cb will get called, or err returned. func (mem *Mempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err error) { - return mem.CheckTxWithInfo(tx, cb, TxInfo{PeerID: UnknownPeerID}) + return mem.CheckTxWithInfo(tx, cb, TxInfo{PeerID: UnknownPeerID, FromPersistent: false}) } // CheckTxWithInfo performs the same operation as CheckTx, but with extra meta data about the tx. @@ -441,7 +443,7 @@ func (mem *Mempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), txInfo } reqRes := mem.proxyAppConn.CheckTxAsync(tx) - reqRes.SetCallback(mem.reqResCb(tx, txInfo.PeerID, cb)) + reqRes.SetCallback(mem.reqResCb(tx, txInfo, cb)) return nil } @@ -474,14 +476,14 @@ func (mem *Mempool) globalCb(req *abci.Request, res *abci.Response) { // when all other response processing is complete. // // Used in CheckTxWithInfo to record PeerID who sent us the tx. -func (mem *Mempool) reqResCb(tx []byte, peerID uint16, externalCb func(*abci.Response)) func(res *abci.Response) { +func (mem *Mempool) reqResCb(tx []byte, txInfo TxInfo, externalCb func(*abci.Response)) func(res *abci.Response) { return func(res *abci.Response) { if mem.recheckCursor != nil { // this should never happen panic("recheck cursor is not nil in reqResCb") } - mem.resCbFirstTime(tx, peerID, res) + mem.resCbFirstTime(tx, txInfo, res) // update metrics mem.metrics.Size.Set(float64(mem.Size())) @@ -520,7 +522,7 @@ func (mem *Mempool) removeTx(tx types.Tx, elem *clist.CElement, removeFromCache // // The case where the app checks the tx for the second and subsequent times is // handled by the resCbRecheck callback. -func (mem *Mempool) resCbFirstTime(tx []byte, peerID uint16, res *abci.Response) { +func (mem *Mempool) resCbFirstTime(tx []byte, txInfo TxInfo, res *abci.Response) { switch r := res.Value.(type) { case *abci.Response_CheckTx: var postCheckErr error @@ -529,17 +531,19 @@ func (mem *Mempool) resCbFirstTime(tx []byte, peerID uint16, res *abci.Response) } if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { memTx := &mempoolTx{ - height: mem.height, - gasWanted: r.CheckTx.GasWanted, - tx: tx, + fromPersistent: txInfo.FromPersistent, + height: mem.height, + gasWanted: r.CheckTx.GasWanted, + tx: tx, } - memTx.senders.Store(peerID, true) + memTx.senders.Store(txInfo.PeerID, true) mem.addTx(memTx) mem.logger.Info("Added good transaction", "tx", TxID(tx), "res", r, "height", memTx.height, "total", mem.Size(), + "fromPersistent", memTx.fromPersistent, ) mem.notifyTxsAvailable() } else { @@ -778,9 +782,10 @@ func (mem *Mempool) recheckTxs(txs []types.Tx) { // mempoolTx is a transaction that successfully ran type mempoolTx struct { - height int64 // height that this tx had been validated in - gasWanted int64 // amount of gas this tx states it will require - tx types.Tx // + fromPersistent bool // whether the tx come from a persistent peer + height int64 // height that this tx had been validated in + gasWanted int64 // amount of gas this tx states it will require + tx types.Tx // // ids of peers who've sent us this tx (as a map for quick lookups). // senders: PeerID -> bool diff --git a/mempool/reactor.go b/mempool/reactor.go index 6f716ecf8..8293f3852 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -7,8 +7,7 @@ import ( "sync" "time" - amino "github.com/tendermint/go-amino" - + "github.com/tendermint/go-amino" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/libs/clist" "github.com/tendermint/tendermint/libs/log" @@ -193,9 +192,8 @@ func (memR *MempoolReactor) receiveImpl(chID byte, src p2p.Peer, msgBytes []byte switch msg := msg.(type) { case *TxMessage: - memR.Mempool.metrics.ReceivedTx.With("peer_id", string(src.ID())).Add(1) peerID := memR.ids.GetForPeer(src) - err := memR.Mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{PeerID: peerID}) + err := memR.Mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{PeerID: peerID, FromPersistent: memR.Switch.IsPersistent(src)}) if err != nil { if err == ErrTxInCache { memR.Mempool.metrics.DuplicateTx.With("peer_id", string(src.ID())).Add(1) @@ -215,7 +213,7 @@ type PeerState interface { // Send new mempool txs to peer. func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { - if !memR.config.Broadcast || (memR.config.OnlyPersistent && !memR.Switch.IsPersistent(peer)) { + if !memR.config.Broadcast || (memR.config.OnlyToPersistent && !memR.Switch.IsPersistent(peer)) { return } @@ -241,34 +239,34 @@ func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { return } } - memTx := next.Value.(*mempoolTx) - - // make sure the peer is up to date - peerState, ok := peer.Get(types.PeerStateKey).(PeerState) - if !ok { - // Peer does not have a state yet. We set it in the consensus reactor, but - // when we add peer in Switch, the order we call reactors#AddPeer is - // different every time due to us using a map. Sometimes other reactors - // will be initialized before the consensus reactor. We should wait a few - // milliseconds and retry. - time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) - continue - } - if peerState.GetHeight() < memTx.Height()-1 { // Allow for a lag of 1 block - time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) - continue - } - - // ensure peer hasn't already sent us this tx - if _, ok := memTx.senders.Load(peerID); !ok { - // send memTx - msg := &TxMessage{Tx: memTx.tx} - success := peer.Send(MempoolChannel, cdc.MustMarshalBinaryBare(msg)) - if !success { + if !memR.config.SkipTxFromPersistent || !memTx.fromPersistent { + // make sure the peer is up to date + peerState, ok := peer.Get(types.PeerStateKey).(PeerState) + if !ok { + // Peer does not have a state yet. We set it in the consensus reactor, but + // when we add peer in Switch, the order we call reactors#AddPeer is + // different every time due to us using a map. Sometimes other reactors + // will be initialized before the consensus reactor. We should wait a few + // milliseconds and retry. time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) continue } + if peerState.GetHeight() < memTx.Height()-1 { // Allow for a lag of 1 block + time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) + continue + } + + // ensure peer hasn't already sent us this tx + if _, ok := memTx.senders.Load(peerID); !ok { + // send memTx + msg := &TxMessage{Tx: memTx.tx} + success := peer.Send(MempoolChannel, cdc.MustMarshalBinaryBare(msg)) + if !success { + time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) + continue + } + } } select { diff --git a/node/node.go b/node/node.go index 60da4071a..cd0bc30b8 100644 --- a/node/node.go +++ b/node/node.go @@ -16,9 +16,10 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/cors" - amino "github.com/tendermint/go-amino" + "github.com/tendermint/go-amino" abci "github.com/tendermint/tendermint/abci/types" bc "github.com/tendermint/tendermint/blockchain" + "github.com/tendermint/tendermint/blockchain/hot" cfg "github.com/tendermint/tendermint/config" cs "github.com/tendermint/tendermint/consensus" "github.com/tendermint/tendermint/crypto/ed25519" @@ -37,8 +38,8 @@ import ( ctypes "github.com/tendermint/tendermint/rpc/core/types" grpccore "github.com/tendermint/tendermint/rpc/grpc" rpcserver "github.com/tendermint/tendermint/rpc/lib/server" - sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/snapshot" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/state/blockindex" bkv "github.com/tendermint/tendermint/state/blockindex/kv" nullblk "github.com/tendermint/tendermint/state/blockindex/null" @@ -123,19 +124,20 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { } // MetricsProvider returns a consensus, p2p and mempool Metrics. -type MetricsProvider func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics) +type MetricsProvider func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics, *hot.Metrics) // DefaultMetricsProvider returns Metrics build using Prometheus client library // if Prometheus is enabled. Otherwise, it returns no-op Metrics. func DefaultMetricsProvider(config *cfg.InstrumentationConfig) MetricsProvider { - return func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics) { + return func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics, *hot.Metrics) { if config.Prometheus { return cs.PrometheusMetrics(config.Namespace, "chain_id", chainID), p2p.PrometheusMetrics(config.Namespace, "chain_id", chainID), mempl.PrometheusMetrics(config.Namespace, "chain_id", chainID), - sm.PrometheusMetrics(config.Namespace, "chain_id", chainID) + sm.PrometheusMetrics(config.Namespace, "chain_id", chainID), + hot.PrometheusMetrics(config.Namespace, "chain_id", chainID) } - return cs.NopMetrics(), p2p.NopMetrics(), mempl.NopMetrics(), sm.NopMetrics() + return cs.NopMetrics(), p2p.NopMetrics(), mempl.NopMetrics(), sm.NopMetrics(), hot.NopMetrics() } } @@ -160,21 +162,22 @@ type Node struct { isListening bool // services - eventBus *types.EventBus // pub/sub for services - stateDB dbm.DB - blockStore *bc.BlockStore // store the blockchain to disk - bcReactor *bc.BlockchainReactor // for fast-syncing - mempoolReactor *mempl.MempoolReactor // for gossipping transactions - consensusState *cs.ConsensusState // latest consensus state - consensusReactor *cs.ConsensusReactor // for participating in the consensus - evidencePool *evidence.EvidencePool // tracking evidence - proxyApp proxy.AppConns // connection to the application - rpcListeners []net.Listener // rpc servers - txIndexer txindex.TxIndexer - blockIndexer blockindex.BlockIndexer - indexerService *txindex.IndexerService + eventBus *types.EventBus // pub/sub for services + stateDB dbm.DB + blockStore *bc.BlockStore // store the blockchain to disk + bcReactor *bc.BlockchainReactor // for fast-syncing + mempoolReactor *mempl.MempoolReactor // for gossipping transactions + consensusState *cs.ConsensusState // latest consensus state + consensusReactor *cs.ConsensusReactor // for participating in the consensus + evidencePool *evidence.EvidencePool // tracking evidence + proxyApp proxy.AppConns // connection to the application + rpcListeners []net.Listener // rpc servers + txIndexer txindex.TxIndexer + blockIndexer blockindex.BlockIndexer + indexerService *txindex.IndexerService blockIndexService *blockindex.IndexerService - prometheusSrv *http.Server + indexHub *sm.IndexHub + prometheusSrv *http.Server } // NewNode returns a new, ready to go, Tendermint Node. @@ -239,7 +242,7 @@ func NewNode(config *cfg.Config, // Transaction indexing var txIndexer txindex.TxIndexer - var txDB dbm.DB // TODO: remove by refactor defaultdbprovider to cache the created db instaces + var txDB dbm.DB // TODO: remove by refactor defaultdbprovider to cache the created db instaces switch config.TxIndex.Indexer { case "kv": store, err := dbProvider(&DBContext{"tx_index", config}) @@ -286,6 +289,17 @@ func NewNode(config *cfg.Config, return nil, err } + csMetrics, p2pMetrics, memplMetrics, smMetrics, hotMetrics := metricsProvider(genDoc.ChainID) + + indexHub := sm.NewIndexHub(state.LastBlockHeight, stateDB, blockStore, eventBus, sm.IndexHubWithMetrics(smMetrics)) + indexHub.RegisterIndexSvc(blockIndexerService) + indexHub.RegisterIndexSvc(txIndexerService) + indexHub.SetLogger(logger.With("module", "indexer_hub")) + err = indexHub.Start() + if err != nil { + return nil, err + } + // Create the handshaker, which calls RequestInfo, sets the AppVersion on the state, // and replays any blocks as necessary to sync tendermint with the app. consensusLogger := logger.With("module", "consensus") @@ -346,8 +360,6 @@ func NewNode(config *cfg.Config, consensusLogger.Info("This node is not a validator", "addr", addr, "pubKey", pubKey) } - csMetrics, p2pMetrics, memplMetrics, smMetrics := metricsProvider(genDoc.ChainID) - // Make MempoolReactor mempool := mempl.NewMempool( config.Mempool, @@ -413,9 +425,19 @@ func NewNode(config *cfg.Config, ) // Make BlockchainReactor - bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync && (config.StateSyncHeight < 0)) + bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync && (config.StateSyncHeight < 0), config.HotSyncReactor, config.HotSync) bcReactor.SetLogger(logger.With("module", "blockchain")) + var hotSyncReactor *hot.BlockchainReactor + if config.HotSyncReactor { + hotSyncLogger := logger.With("module", "hotsync") + hotSyncReactor = hot.NewBlockChainReactor(state.Copy(), blockExec, blockStore, config.HotSync, fastSync || config.StateSyncHeight >= 0, config.HotSyncTimeout, hot.WithMetrics(hotMetrics), hot.WithEventBus(eventBus)) + hotSyncReactor.SetLogger(hotSyncLogger) + if privValidator != nil { + hotSyncReactor.SetPrivValidator(privValidator) + } + } + // Make ConsensusReactor consensusState := cs.NewConsensusState( config.Consensus, @@ -430,7 +452,7 @@ func NewNode(config *cfg.Config, if privValidator != nil { consensusState.SetPrivValidator(privValidator) } - consensusReactor := cs.NewConsensusReactor(consensusState, fastSync || (config.StateSyncHeight >= 0), cs.ReactorMetrics(csMetrics)) + consensusReactor := cs.NewConsensusReactor(consensusState, fastSync || (config.StateSyncHeight >= 0) || config.HotSync, cs.ReactorMetrics(csMetrics)) consensusReactor.SetLogger(consensusLogger) // services which will be publishing and/or subscribing for messages (events) @@ -519,6 +541,9 @@ func NewNode(config *cfg.Config, if config.StateSyncReactor { sw.AddReactor("STATE", stateReactor) } + if config.HotSyncReactor { + sw.AddReactor("HOT", hotSyncReactor) + } sw.AddReactor("BLOCKCHAIN", bcReactor) sw.AddReactor("CONSENSUS", consensusReactor) sw.AddReactor("EVIDENCE", evidenceReactor) @@ -583,19 +608,20 @@ func NewNode(config *cfg.Config, nodeInfo: nodeInfo, nodeKey: nodeKey, - stateDB: stateDB, - blockStore: blockStore, - bcReactor: bcReactor, - mempoolReactor: mempoolReactor, - consensusState: consensusState, - consensusReactor: consensusReactor, - evidencePool: evidencePool, - proxyApp: proxyApp, - txIndexer: txIndexer, - blockIndexer: blockIndexer, - indexerService: txIndexerService, + stateDB: stateDB, + blockStore: blockStore, + bcReactor: bcReactor, + mempoolReactor: mempoolReactor, + consensusState: consensusState, + consensusReactor: consensusReactor, + evidencePool: evidencePool, + proxyApp: proxyApp, + txIndexer: txIndexer, + blockIndexer: blockIndexer, + indexerService: txIndexerService, blockIndexService: blockIndexerService, - eventBus: eventBus, + indexHub: indexHub, + eventBus: eventBus, } node.BaseService = *cmn.NewBaseService(logger, "Node", node) return node, nil @@ -666,6 +692,7 @@ func (n *Node) OnStop() { n.eventBus.Stop() n.indexerService.Stop() n.blockIndexService.Stop() + n.indexHub.Stop() // now stop the reactors // TODO: gracefully disconnect from peers. @@ -719,6 +746,7 @@ func (n *Node) ConfigureRPC() { rpccore.SetProxyAppQuery(n.proxyApp.Query()) rpccore.SetTxIndexer(n.txIndexer) rpccore.SetBlockIndexer(n.blockIndexer) + rpccore.SetIndexHub(n.indexHub) rpccore.SetConsensusReactor(n.consensusReactor) rpccore.SetEventBus(n.eventBus) rpccore.SetLogger(n.Logger.With("module", "rpc")) @@ -738,7 +766,7 @@ func (n *Node) startRPC() ([]net.Listener, error) { // we may expose the rpc over both a unix and tcp socket listeners := make([]net.Listener, len(listenAddrs)) var wsWorkerPool *gopool.Pool - if n.config.RPC.WebsocketPoolMaxSize > 1{ + if n.config.RPC.WebsocketPoolMaxSize > 1 { wsWorkerPool = gopool.NewPool(n.config.RPC.WebsocketPoolMaxSize, n.config.RPC.WebsocketPoolQueueSize, n.config.RPC.WebsocketPoolSpawnSize) wsWorkerPool.SetLogger(n.Logger.With("module", "routine-pool")) } @@ -949,6 +977,10 @@ func makeNodeInfo( nodeInfo.Channels = append(nodeInfo.Channels, pex.PexChannel) } + if config.HotSyncReactor { + nodeInfo.Channels = append(nodeInfo.Channels, hot.HotBlockchainChannel) + } + lAddr := config.P2P.ExternalAddress if lAddr == "" { diff --git a/p2p/conn/connection.go b/p2p/conn/connection.go index 289769f5b..207e3f7fd 100644 --- a/p2p/conn/connection.go +++ b/p2p/conn/connection.go @@ -322,8 +322,6 @@ func (c *MConnection) Send(chID byte, msgBytes []byte) bool { return false } - c.Logger.Debug("Send", "channel", chID, "conn", c, "msgBytes", fmt.Sprintf("%X", msgBytes)) - // Send message to channel. channel, ok := c.channelsIdx[chID] if !ok { @@ -594,7 +592,6 @@ FOR_LOOP: break FOR_LOOP } if msgBytes != nil { - c.Logger.Debug("Received bytes", "chID", pkt.ChannelID, "msgBytes", fmt.Sprintf("%X", msgBytes)) // NOTE: This means the reactor.Receive runs in the same thread as the p2p recv routine // except the mempool actually is using an asynchronus Receive() to prevent jamming requests // stopping block producing (tested via ) diff --git a/p2p/peer.go b/p2p/peer.go index fab3b42d4..ac5072933 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -247,9 +247,6 @@ func (p *peer) Send(chID byte, msgBytes []byte) bool { return false } res := p.mconn.Send(chID, msgBytes) - if res { - p.metrics.PeerSendBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) - } return res } @@ -262,9 +259,6 @@ func (p *peer) TrySend(chID byte, msgBytes []byte) bool { return false } res := p.mconn.TrySend(chID, msgBytes) - if res { - p.metrics.PeerSendBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) - } return res } @@ -369,7 +363,6 @@ func createMConnection( // which does onPeerError. panic(fmt.Sprintf("Unknown channel %X", chID)) } - p.metrics.PeerReceiveBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) reactor.Receive(chID, p, msgBytes) } diff --git a/p2p/switch.go b/p2p/switch.go index 0a5d3cf90..a849fc90b 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -69,16 +69,17 @@ type PeerFilterFunc func(IPeerSet, Peer) error type Switch struct { cmn.BaseService - config *config.P2PConfig - reactors map[string]Reactor - chDescs []*conn.ChannelDescriptor - reactorsByCh map[byte]Reactor - peers *PeerSet - dialing *cmn.CMap - reconnecting *cmn.CMap - nodeInfo NodeInfo // our node info - nodeKey *NodeKey // our node privkey - addrBook AddrBook + config *config.P2PConfig + reactors map[string]Reactor + chDescs []*conn.ChannelDescriptor + reactorsByCh map[byte]Reactor + peers *PeerSet + dialing *cmn.CMap + reconnecting *cmn.CMap + nodeInfo NodeInfo // our node info + nodeKey *NodeKey // our node privkey + addrBook AddrBook + persistentPeers map[ID]bool transport Transport @@ -120,7 +121,7 @@ func NewSwitch( // Ensure we have a completely undeterministic PRNG. sw.rng = cmn.NewRand() - + sw.initPersistentPeers() sw.BaseService = *cmn.NewBaseService(nil, "P2P Switch", sw) for _, option := range options { @@ -197,18 +198,19 @@ func (sw *Switch) SetNodeKey(nodeKey *NodeKey) { sw.nodeKey = nodeKey } -func (sw *Switch) IsPersistent(peer Peer) bool { - if sw.config.PersistentPeers == "" { - return false - } - peers := cmn.SplitAndTrim(sw.config.PersistentPeers, ",", " ") - netAddrs, _ := NewNetAddressStrings(peers) - for _, addr := range netAddrs { - if addr.ID == peer.ID() { - return true +func (sw *Switch) initPersistentPeers() { + sw.persistentPeers = make(map[ID]bool, 0) + if sw.config.PersistentPeers != "" { + peers := cmn.SplitAndTrim(sw.config.PersistentPeers, ",", " ") + netAddrs, _ := NewNetAddressStrings(peers) + for _, addr := range netAddrs { + sw.persistentPeers[addr.ID] = true } } - return false +} + +func (sw *Switch) IsPersistent(peer Peer) bool { + return sw.persistentPeers[peer.ID()] } //--------------------------------------------------------------------- diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 5e830ccba..5b49b05a9 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -405,6 +405,7 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { defer rp.Stop() addr := *rp.Addr() sw.config.PersistentPeers = addr.String() + sw.initPersistentPeers() p, err := sw.transport.Dial(addr, peerConfig{ chDescs: sw.chDescs, @@ -448,6 +449,7 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { conf := config.DefaultP2PConfig() conf.TestDialFail = true sw.config.PersistentPeers = fmt.Sprintf("%s,%s", sw.config.PersistentPeers, rp.Addr().String()) + sw.initPersistentPeers() err = sw.addOutboundPeerWithConfig(rp.Addr(), conf, true) require.NotNil(err) diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 6f1579b6d..43eabf53b 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -75,6 +75,7 @@ var ( consensusReactor *consensus.ConsensusReactor eventBus *types.EventBus // thread safe mempool *mempl.Mempool + indexerHub *sm.IndexHub logger log.Logger @@ -133,6 +134,10 @@ func SetBlockIndexer(indexer blockindex.BlockIndexer) { blockIndexer = indexer } +func SetIndexHub(hub *sm.IndexHub) { + indexerHub = hub +} + func SetConsensusReactor(conR *consensus.ConsensusReactor) { consensusReactor = conR } diff --git a/rpc/core/status.go b/rpc/core/status.go index f2e0624d4..f2f0ddb82 100644 --- a/rpc/core/status.go +++ b/rpc/core/status.go @@ -58,7 +58,8 @@ import ( // "latest_app_hash": "0000000000000000", // "latest_block_height": "18", // "latest_block_time": "2018-09-17T11:42:19.149920551Z", -// "catching_up": false +// "catching_up": false, +// "index_height": "18" // }, // "validator_info": { // "address": "D9F56456D7C5793815D0E9AF07C3A355D0FC64FD", @@ -106,6 +107,7 @@ func Status(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { LatestBlockHeight: latestHeight, LatestBlockTime: latestBlockTime, CatchingUp: consensusReactor.FastSync(), + IndexHeight: indexerHub.GetHeight(), }, ValidatorInfo: ctypes.ValidatorInfo{ Address: pubKey.Address(), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 74457b38a..075fa957a 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -63,6 +63,7 @@ type SyncInfo struct { LatestBlockHeight int64 `json:"latest_block_height"` LatestBlockTime time.Time `json:"latest_block_time"` CatchingUp bool `json:"catching_up"` + IndexHeight int64 `json:"index_height"` } // Info about the node's validator diff --git a/snapshot/reactor.go b/snapshot/reactor.go index 5fa8da7f3..b5f683dd0 100644 --- a/snapshot/reactor.go +++ b/snapshot/reactor.go @@ -27,10 +27,10 @@ const ( StateSyncChannel = byte(0x36) // ====== move to config ====== - stateSyncPriority = 10 // channel priority of state sync - tryManifestFinalizeSeconds = 10 // how often to check whether we collect enough manifests - maxTriedManifestFinalist = 3 // max round of try finalize manifest - leastPeersToSync = 3 // how many peers with same manifest hash before we can start state sync + stateSyncPriority = 10 // channel priority of state sync + tryManifestFinalizeSeconds = 10 // how often to check whether we collect enough manifests + maxTriedManifestFinalist = 3 // max round of try finalize manifest + leastPeersToSync = 3 // how many peers with same manifest hash before we can start state sync // NOTE: keep up to date with bcChunkResponseMessage // TODO: REVIEW before final merge @@ -39,7 +39,7 @@ const ( maxStateMsgSize = types.MaxStateSizeBytes + bcStateResponseMessagePrefixSize + bcStateResponseMessageFieldKeySize - maxInFlightRequesters = 600 + maxInFlightRequesters = 600 maxInflightRequestPerPeer = 20 @@ -88,11 +88,11 @@ func NewStateReactor(stateDB dbm.DB, app proxy.AppConnState, config *cfg.Config) } bcSR := &StateReactor{ - config: config, - pool: pool, - stateSync: config.StateSyncHeight, - requestsCh: requestsCh, - errorsCh: errorsCh, + config: config, + pool: pool, + stateSync: config.StateSyncHeight, + requestsCh: requestsCh, + errorsCh: errorsCh, } bcSR.BaseReactor = *p2p.NewBaseReactor("StateSyncReactor", bcSR) return bcSR @@ -290,7 +290,7 @@ func (bcSR *StateReactor) poolRoutine() { go func() { for { select { - case <- bcSR.pool.Quit(): + case <-bcSR.pool.Quit(): return case <-bcSR.Quit(): @@ -332,7 +332,7 @@ func (bcSR *StateReactor) poolRoutine() { // always broadcast state status request to get more potential trusted peers even pool already inited bcSR.BroadcastStateStatusRequest() - case <- bcSR.pool.Quit(): + case <-bcSR.pool.Quit(): return case <-bcSR.Quit(): @@ -382,7 +382,7 @@ func (bcSR *StateReactor) decodeMsg(bz []byte) (msg BlockchainStateMessage, err type bcChunkRequestMessage struct { Height int64 - Hash abci.SHA256Sum + Hash abci.SHA256Sum } func (m *bcChunkRequestMessage) String() string { @@ -391,7 +391,7 @@ func (m *bcChunkRequestMessage) String() string { type bcNoChunkResponseMessage struct { Height int64 - Hash abci.SHA256Sum + Hash abci.SHA256Sum } func (m *bcNoChunkResponseMessage) String() string { diff --git a/state/blockindex/indexer_service.go b/state/blockindex/indexer_service.go index 2a4b3bf81..f41768e0f 100644 --- a/state/blockindex/indexer_service.go +++ b/state/blockindex/indexer_service.go @@ -19,6 +19,8 @@ type IndexerService struct { idr BlockIndexer eventBus *types.EventBus + + onIndex func(int64) } // NewIndexerService returns a new service instance. @@ -28,6 +30,10 @@ func NewIndexerService(idr BlockIndexer, eventBus *types.EventBus) *IndexerServi return is } +func (is *IndexerService) SetOnIndex(callback func(int64)) { + is.onIndex = callback +} + // OnStart implements cmn.Service by subscribing for blocks and indexing them by hash. func (is *IndexerService) OnStart() error { blockHeadersSub, err := is.eventBus.SubscribeUnbuffered(context.Background(), subscriber, types.EventQueryNewBlockHeader) @@ -45,6 +51,9 @@ func (is *IndexerService) OnStart() error { } else { is.Logger.Info("Indexed block", "height", header.Height, "hash", header.LastBlockID.Hash) } + if is.onIndex != nil { + is.onIndex(header.Height) + } } }() return nil diff --git a/state/execution.go b/state/execution.go index 78ce171fb..07e76c9ad 100644 --- a/state/execution.go +++ b/state/execution.go @@ -54,13 +54,13 @@ func BlockExecutorWithMetrics(metrics *Metrics) BlockExecutorOption { func NewBlockExecutor(db dbm.DB, logger log.Logger, proxyApp proxy.AppConnConsensus, mempool Mempool, evpool EvidencePool, withAppState bool, options ...BlockExecutorOption) *BlockExecutor { res := &BlockExecutor{ - db: db, - proxyApp: proxyApp, - eventBus: types.NopEventBus{}, - mempool: mempool, - evpool: evpool, - logger: logger, - metrics: NopMetrics(), + db: db, + proxyApp: proxyApp, + eventBus: types.NopEventBus{}, + mempool: mempool, + evpool: evpool, + logger: logger, + metrics: NopMetrics(), withAppState: withAppState, } diff --git a/state/index.go b/state/index.go new file mode 100644 index 000000000..82b4b701e --- /dev/null +++ b/state/index.go @@ -0,0 +1,181 @@ +package state + +import ( + "sync" + + cmn "github.com/tendermint/tendermint/libs/common" + dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/types" + abci "github.com/tendermint/tendermint/abci/types" + +) + +const ( + MaxIndexLag = 100 +) + +var IndexHeightKey = []byte("IndexHeightKey") + +type IndexService interface { + SetOnIndex(callback func(int64)) +} + +type IndexHub struct { + cmn.BaseService + mtx sync.Mutex + + stateHeight int64 + expectHeight int64 + + // the total registered index service + numIdxSvc int + indexTaskCounter map[int64]int + indexTaskEvents chan int64 + + stateDB dbm.DB + blockStore BlockStore + eventBus types.BlockEventPublisher + + metrics *Metrics +} + +func NewIndexHub(initialHeight int64, stateDB dbm.DB, blockStore BlockStore, eventBus types.BlockEventPublisher, options ...IndexHubOption) *IndexHub { + ih := &IndexHub{ + stateHeight: initialHeight, + indexTaskCounter: make(map[int64]int, 0), + indexTaskEvents: make(chan int64, MaxIndexLag), + stateDB: stateDB, + blockStore: blockStore, + eventBus: eventBus, + metrics: NopMetrics(), + } + indexedHeight := ih.GetIndexedHeight() + if indexedHeight < 0 { + // no indexedHeight found, will do no recover + ih.expectHeight = ih.stateHeight + 1 + } else { + ih.expectHeight = indexedHeight + 1 + } + for _, option := range options { + option(ih) + } + ih.BaseService = *cmn.NewBaseService(nil, "indexHub", ih) + return ih +} + +type IndexHubOption func(*IndexHub) + +func IndexHubWithMetrics(metrics *Metrics) IndexHubOption { + return func(ih *IndexHub) { + ih.metrics = metrics + } +} + +func (ih *IndexHub) OnStart() error { + // start listen routine before recovering. + go ih.commitIndexRoutine() + ih.recoverIndex() + return nil +} + +func (ih *IndexHub) recoverIndex() { + for h := ih.expectHeight; h <= ih.stateHeight; h++ { + ih.Logger.Info("try to recover index", "height", h) + block := ih.blockStore.LoadBlock(h) + if block == nil { + ih.Logger.Error("index skip since the the block is missing", "height", h) + } else { + abciResponses, err := LoadABCIResponses(ih.stateDB, h) + if err != nil { + ih.Logger.Error("failed to load ABCIResponse, will use default") + abciResponses = NewABCIResponses(block) + abciResponses.EndBlock = &abci.ResponseEndBlock{} + abciResponses.BeginBlock = &abci.ResponseBeginBlock{} + } + abciValUpdates := abciResponses.EndBlock.ValidatorUpdates + validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciValUpdates) + if err != nil { + ih.Logger.Error("failed to load validatorUpdates, will use nil by default") + } + fireEvents(ih.Logger, ih.eventBus, block, abciResponses, validatorUpdates) + } + } +} + +func (ih *IndexHub) commitIndexRoutine() { + for { + select { + case <-ih.Quit(): + return + case h := <-ih.indexTaskEvents: + ih.Logger.Info("finish index", "height", h) + ih.SetIndexedHeight(h) + ih.metrics.IndexHeight.Set(float64(h)) + } + } +} + +func (ih *IndexHub) RegisterIndexSvc(idx IndexService) { + ih.mtx.Lock() + defer ih.mtx.Unlock() + if ih.IsRunning() { + panic("can't RegisterIndexSvc when IndexHub is running") + } + idx.SetOnIndex(ih.CountDownAt) + ih.numIdxSvc++ +} + +// `CountDownAt` is a callback in index service, keep it simple and fast. +func (ih *IndexHub) CountDownAt(height int64) { + ih.mtx.Lock() + defer ih.mtx.Unlock() + count, exist := ih.indexTaskCounter[height] + if exist { + count = count - 1 + } else { + count = ih.numIdxSvc - 1 + } + // The higher block won't finish index before lower one. + if count == 0 && height == ih.expectHeight { + if exist { + delete(ih.indexTaskCounter, height) + } + ih.expectHeight = ih.expectHeight + 1 + ih.indexTaskEvents <- height + } else { + ih.indexTaskCounter[height] = count + } +} + +// set and get won't happen in the same time, won't lock +func (ih *IndexHub) SetIndexedHeight(h int64) { + rawHeight, err := cdc.MarshalBinaryBare(h) + if err != nil { + ih.Logger.Error("failed to MarshalBinaryBare for indexed height", "error", err, "height", h) + } else { + ih.stateDB.Set(IndexHeightKey, rawHeight) + } +} + +// if never store `IndexHeightKey` in index db, will return -1. +func (ih *IndexHub) GetIndexedHeight() int64 { + rawHeight := ih.stateDB.Get(IndexHeightKey) + if rawHeight == nil { + return -1 + } else { + var height int64 + err := cdc.UnmarshalBinaryBare(rawHeight, &height) + if err != nil { + // should not happen + panic(err) + } + return height + } +} + +// get indexed height from memory to save time for RPC +func (ih *IndexHub) GetHeight() int64 { + ih.mtx.Lock() + defer ih.mtx.Unlock() + return ih.expectHeight - 1 +} diff --git a/state/index_test.go b/state/index_test.go new file mode 100644 index 000000000..041f8a98f --- /dev/null +++ b/state/index_test.go @@ -0,0 +1,90 @@ +package state + +import ( + "math/rand" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/state/blockindex" + kv2 "github.com/tendermint/tendermint/state/blockindex/kv" + "github.com/tendermint/tendermint/state/txindex" + "github.com/tendermint/tendermint/state/txindex/kv" + "github.com/tendermint/tendermint/types" +) + +func TestSetHeight(t *testing.T) { + + indexDb := db.NewMemDB() + indexHub := NewIndexHub(0, indexDb, nil, nil) + indexHub.SetLogger(log.TestingLogger()) + + realHeightAtFirst := indexHub.GetIndexedHeight() + assert.Equal(t, int64(-1), realHeightAtFirst) + height := int64(1024) + indexHub.SetIndexedHeight(height) + realHeight := indexHub.GetIndexedHeight() + assert.Equal(t, height, realHeight) +} + +func TestCountDown(t *testing.T) { + // event bus + eventBus := types.NewEventBus() + eventBus.SetLogger(log.TestingLogger()) + err := eventBus.Start() + require.NoError(t, err) + defer eventBus.Stop() + + indexDb := db.NewMemDB() + + // start tx index + txIndexer := kv.NewTxIndex(indexDb, kv.IndexAllTags()) + txIndexSvc := txindex.NewIndexerService(txIndexer, eventBus) + txIndexSvc.SetLogger(log.TestingLogger()) + err = txIndexSvc.Start() + require.NoError(t, err) + defer txIndexSvc.Stop() + + // start block index + blockIndexer := kv2.NewBlockIndex(indexDb) + blockIndexSvc := blockindex.NewIndexerService(blockIndexer, eventBus) + blockIndexSvc.SetLogger(log.TestingLogger()) + err = blockIndexSvc.Start() + require.NoError(t, err) + defer blockIndexSvc.Stop() + + // start index hub + indexHub := NewIndexHub(0, indexDb, nil, eventBus) + indexHub.SetLogger(log.TestingLogger()) + indexHub.RegisterIndexSvc(txIndexSvc) + indexHub.RegisterIndexSvc(blockIndexSvc) + err = indexHub.Start() + assert.NoError(t, err) + + // publish block with txs + for h := int64(1); h < 10; h++ { + numTxs := rand.Int63n(5) + eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{ + Header: types.Header{Height: h, NumTxs: numTxs}, + }) + for i := int64(0); i < numTxs; i++ { + txResult := &types.TxResult{ + Height: h, + Index: uint32(i), + Tx: types.Tx("foo"), + Result: abci.ResponseDeliverTx{Code: 0}, + } + eventBus.PublishEventTx(types.EventDataTx{*txResult}) + } + // In test case, 100ms is far enough for index + time.Sleep(100 * time.Millisecond) + assert.Equal(t, int64(h), indexHub.GetIndexedHeight()) + // test no memory leak + assert.Equal(t, len(indexHub.indexTaskCounter), 0) + } +} diff --git a/state/metrics.go b/state/metrics.go index bcd713f5f..23f01225e 100644 --- a/state/metrics.go +++ b/state/metrics.go @@ -17,6 +17,8 @@ const ( type Metrics struct { // Time between BeginBlock and EndBlock. BlockProcessingTime metrics.Histogram + // The latest height of block that have been indexed + IndexHeight metrics.Gauge } // PrometheusMetrics returns Metrics build using Prometheus client library. @@ -35,6 +37,12 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { Help: "Time between BeginBlock and EndBlock in ms.", Buckets: stdprometheus.LinearBuckets(1, 10, 10), }, labels).With(labelsAndValues...), + IndexHeight: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ + Namespace: namespace, + Subsystem: MetricsSubsystem, + Name: "index_height", + Help: "The latest height of block that have been indexed", + }, append(labels)).With(labelsAndValues...), } } @@ -42,5 +50,6 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { func NopMetrics() *Metrics { return &Metrics{ BlockProcessingTime: discard.NewHistogram(), + IndexHeight: discard.NewGauge(), } } diff --git a/state/store.go b/state/store.go index b9cf119ab..5938b202e 100644 --- a/state/store.go +++ b/state/store.go @@ -22,7 +22,7 @@ const ( const latestStateToKeep int64 = 1 << 20 func calcStateKey(height int64) []byte { - return []byte(fmt.Sprintf("stateKey:%v", height % latestStateToKeep)) + return []byte(fmt.Sprintf("stateKey:%v", height%latestStateToKeep)) } func calcValidatorsKey(height int64) []byte { diff --git a/state/txindex/indexer_service.go b/state/txindex/indexer_service.go index 03fa9c03e..de9d83688 100644 --- a/state/txindex/indexer_service.go +++ b/state/txindex/indexer_service.go @@ -19,6 +19,8 @@ type IndexerService struct { idr TxIndexer eventBus *types.EventBus + + onIndex func(int64) } // NewIndexerService returns a new service instance. @@ -28,6 +30,10 @@ func NewIndexerService(idr TxIndexer, eventBus *types.EventBus) *IndexerService return is } +func (is *IndexerService) SetOnIndex(callback func(int64)) { + is.onIndex = callback +} + // OnStart implements cmn.Service by subscribing for all transactions // and indexing them by tags. func (is *IndexerService) OnStart() error { @@ -65,6 +71,9 @@ func (is *IndexerService) OnStart() error { } else { is.Logger.Info("Indexed txs for block", "height", header.Height) } + if is.onIndex != nil { + is.onIndex(header.Height) + } } }() return nil diff --git a/types/event_bus.go b/types/event_bus.go index da959090f..9a9d34b3e 100644 --- a/types/event_bus.go +++ b/types/event_bus.go @@ -189,6 +189,10 @@ func (b *EventBus) PublishEventCompleteProposal(data EventDataCompleteProposal) return b.Publish(EventCompleteProposal, data) } +func (b *EventBus) PublishEventMajorPrecommits(data EventDataMajorPrecommits) error { + return b.Publish(EventMajorPrecommits, data) +} + func (b *EventBus) PublishEventPolka(data EventDataRoundState) error { return b.Publish(EventPolka, data) } diff --git a/types/events.go b/types/events.go index b65ea3832..297d476f6 100644 --- a/types/events.go +++ b/types/events.go @@ -34,6 +34,7 @@ const ( EventTimeoutWait = "TimeoutWait" EventUnlock = "Unlock" EventValidBlock = "ValidBlock" + EventMajorPrecommits = "MajorPrecommits" EventVote = "Vote" ) @@ -108,6 +109,15 @@ type EventDataCompleteProposal struct { Step string `json:"step"` BlockID BlockID `json:"block_id"` + Block Block `json:"block"` +} + +type EventDataMajorPrecommits struct { + Height int64 `json:"height"` + Round int `json:"round"` + Step string `json:"step"` + + Votes VoteSet `json:"votes"` } type EventDataVote struct { @@ -137,6 +147,7 @@ const ( var ( EventQueryCompleteProposal = QueryForEvent(EventCompleteProposal) + EventQueryMajorPrecommits = QueryForEvent(EventMajorPrecommits) EventQueryLock = QueryForEvent(EventLock) EventQueryNewBlock = QueryForEvent(EventNewBlock) EventQueryNewBlockHeader = QueryForEvent(EventNewBlockHeader) diff --git a/types/vote_set.go b/types/vote_set.go index 1cd0f2281..81b6a0c8c 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -119,6 +119,21 @@ func (voteSet *VoteSet) Size() int { return voteSet.valSet.Size() } +func (voteSet *VoteSet) Copy() *VoteSet { + if voteSet == nil { + return nil + } + return &VoteSet{ + chainID: voteSet.chainID, + height: voteSet.height, + round: voteSet.round, + type_: voteSet.type_, + valSet: voteSet.valSet, + votes: voteSet.votes, + maj23: voteSet.maj23, + } +} + // Returns added=true if vote is valid and new. // Otherwise returns err=ErrVote[ // UnexpectedStep | InvalidIndex | InvalidAddress | From cf3da3a25adb5bbec2b738c56e86f16193652a79 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 19 Jul 2019 19:29:33 +0200 Subject: [PATCH 143/211] docs: add A TOC to the Readme.md of ADR Section (#3820) * ADR TOC in readme.md * Added A TOC to the Readme.md of ADR Section - Added table of contents to the Readme of the architecture section. - Easier to traverse and when you know what is there. - If the Adr's become viewable online it would help guide the user Signed-off-by: Marko Baricevic * add tm-cmn to subprojects * normalize word --- README.md | 40 ++++++++++++++++++------------------- docs/architecture/README.md | 38 +++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 994ca63b2..3ea9d5de4 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,21 @@ # Tendermint + ![banner](docs/tendermint-core-image.jpg) [Byzantine-Fault Tolerant](https://en.wikipedia.org/wiki/Byzantine_fault_tolerance) [State Machines](https://en.wikipedia.org/wiki/State_machine_replication). -Or [Blockchain](https://en.wikipedia.org/wiki/Blockchain_(database)), for short. +Or [Blockchain](), for short. [![version](https://img.shields.io/github/tag/tendermint/tendermint.svg)](https://github.com/tendermint/tendermint/releases/latest) -[![API Reference]( -https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f676f6c616e672f6764646f3f7374617475732e737667 -)](https://godoc.org/github.com/tendermint/tendermint) +[![API Reference](https://camo.githubusercontent.com/915b7be44ada53c290eb157634330494ebe3e30a/68747470733a2f2f676f646f632e6f72672f6769746875622e636f6d2f676f6c616e672f6764646f3f7374617475732e737667)](https://godoc.org/github.com/tendermint/tendermint) [![Go version](https://img.shields.io/badge/go-1.12.0-blue.svg)](https://github.com/moovweb/gvm) [![riot.im](https://img.shields.io/badge/riot.im-JOIN%20CHAT-green.svg)](https://riot.im/app/#/room/#tendermint:matrix.org) [![license](https://img.shields.io/github/license/tendermint/tendermint.svg)](https://github.com/tendermint/tendermint/blob/master/LICENSE) [![](https://tokei.rs/b1/github/tendermint/tendermint?category=lines)](https://github.com/tendermint/tendermint) - -Branch | Tests | Coverage -----------|-------|---------- -master | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/master.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/master) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/master/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) +| Branch | Tests | Coverage | +| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| master | [![CircleCI](https://circleci.com/gh/tendermint/tendermint/tree/master.svg?style=shield)](https://circleci.com/gh/tendermint/tendermint/tree/master) | [![codecov](https://codecov.io/gh/tendermint/tendermint/branch/master/graph/badge.svg)](https://codecov.io/gh/tendermint/tendermint) | Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state transition machine - written in any programming language - and securely replicates it on many machines. @@ -49,9 +47,9 @@ For examples of the kinds of bugs we're looking for, see [SECURITY.md](SECURITY. ## Minimum requirements -Requirement|Notes ----|--- -Go version | Go1.11.4 or higher +| Requirement | Notes | +| ----------- | ------------------ | +| Go version | Go1.11.4 or higher | ## Documentation @@ -145,20 +143,20 @@ Additional documentation is found [here](/docs/tools). ### Sub-projects -* [Amino](http://github.com/tendermint/go-amino), reflection-based proto3, with +- [Amino](http://github.com/tendermint/go-amino), reflection-based proto3, with interfaces -* [IAVL](http://github.com/tendermint/iavl), Merkleized IAVL+ Tree implementation +- [IAVL](http://github.com/tendermint/iavl), Merkleized IAVL+ Tree implementation +- [Tm-cmn](http://github.com/tendermint/tm-cmn), Commonly used libs across Tendermint & Cosmos repos ### Applications -* [Cosmos SDK](http://github.com/cosmos/cosmos-sdk); a cryptocurrency application framework -* [Ethermint](http://github.com/cosmos/ethermint); Ethereum on Tendermint -* [Many more](https://tendermint.com/ecosystem) +- [Cosmos SDK](http://github.com/cosmos/cosmos-sdk); a cryptocurrency application framework +- [Ethermint](http://github.com/cosmos/ethermint); Ethereum on Tendermint +- [Many more](https://tendermint.com/ecosystem) ### Research -* [The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938) -* [Master's Thesis on Tendermint](https://atrium.lib.uoguelph.ca/xmlui/handle/10214/9769) -* [Original Whitepaper](https://tendermint.com/static/docs/tendermint.pdf) -* [Blog](https://blog.cosmos.network/tendermint/home) - +- [The latest gossip on BFT consensus](https://arxiv.org/abs/1807.04938) +- [Master's Thesis on Tendermint](https://atrium.lib.uoguelph.ca/xmlui/handle/10214/9769) +- [Original Whitepaper](https://tendermint.com/static/docs/tendermint.pdf) +- [Blog](https://blog.cosmos.network/tendermint/home) diff --git a/docs/architecture/README.md b/docs/architecture/README.md index 1cfc7ddce..0ff6682ac 100644 --- a/docs/architecture/README.md +++ b/docs/architecture/README.md @@ -20,3 +20,41 @@ it stands today. If recorded decisions turned out to be lacking, convene a discussion, record the new decisions here, and then modify the code to match. Note the context/background should be written in the present tense. + +### Table of Contents: + +- [ADR-001-Logging](./adr-001-logging.md) +- [ADR-002-Event-Subscription](./adr-002-event-subscription.md) +- [ADR-003-ABCI-APP-RPC](./adr-003-abci-app-rpc.md) +- [ADR-004-Historical-Validators](./adr-004-historical-validators.md) +- [ADR-005-Consensus-Params](./adr-005-consensus-params.md) +- [ADR-006-Trust-Metric](./adr-006-trust-metric.md) +- [ADR-007-Trust-Metric-Usage](./adr-007-trust-metric-usage.md) +- [ADR-008-Priv-Validator](./adr-008-priv-validator.md) +- [ADR-009-ABCI-Design](./adr-009-abci-design.md) +- [ADR-010-Crypto-Changes](./adr-010-crypto-changes.md) +- [ADR-011-Monitoring](./adr-011-monitoring.md) +- [ADR-012-Peer-Transport](./adr-012-peer-transport.md) +- [ADR-013-Symmetric-Crypto](./adr-013-symmetric-crypto.md) +- [ADR-014-Secp-Malleability](./adr-014-secp-malleability.md) +- [ADR-015-Crypto-Encoding](./adr-015-crypto-encoding.md) +- [ADR-016-Protocol-Versions](./adr-016-protocol-versions.md) +- [ADR-017-Chain-Versions](./adr-017-chain-versions.md) +- [ADR-018-ABCI-Validators](./adr-018-abci-validators.md) +- [ADR-019-Multisigs](./adr-019-multisigs.md) +- [ADR-020-Block-Size](./adr-020-block-size.md) +- [ADR-021-ABCI-Events](./adr-021-abci-events.md) +- [ADR-022-ABCI-Errors](./adr-022-abci-errors.md) +- [ADR-023-ABCI-Propose-tx](./adr-023-ABCI-propose-tx.md) +- [ADR-024-Sign-Bytes](./adr-024-sign-bytes.md) +- [ADR-025-Commit](./adr-025-commit.md) +- [ADR-026-General-Merkle-Proof](./adr-026-general-merkle-proof.md) +- [ADR-029-Check-Tx-Consensus](./adr-029-check-tx-consensus.md) +- [ADR-030-Consensus-Refactor](./adr-030-consensus-refactor.md) +- [ADR-033-Pubsub](./adr-033-pubsub.md) +- [ADR-034-Priv-Validator-File-Structure](./adr-034-priv-validator-file-structure.md) +- [ADR-035-Documentation](./adr-035-documentation.md) +- [ADR-037-Deliver-Block](./adr-037-deliver-block.md) +- [ADR-039-Peer-Behaviour](./adr-039-peer-behaviour.md) +- [ADR-041-Proposer-Selection-via-ABCI](./adr-041-proposer-selection-via-abci.md) +- [ADR-043-Blockchain-RiRi-Org](./adr-043-blockchain-riri-org.md) From db65b72f5edbed4a3f5c9972b698e8a61ae0b7a1 Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Sat, 20 Jul 2019 16:44:42 +0900 Subject: [PATCH 144/211] rpc: make max_body_bytes and max_header_bytes configurable (#3818) * rpc: make max_body_bytes and max_header_bytes configurable * update changelog pending --- CHANGELOG_PENDING.md | 1 + config/config.go | 15 +++++++++++++++ config/toml.go | 6 ++++++ docs/tendermint-core/configuration.md | 6 ++++++ node/node.go | 25 ++++++++++++++----------- rpc/lib/server/handlers.go | 13 ++++++++++++- rpc/lib/server/http_server.go | 24 +++++++++++------------- 7 files changed, 65 insertions(+), 25 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 41400d892..416423637 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -19,5 +19,6 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: - [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) +- [rpc] \#3818 Make `max_body_bytes` and `max_header_bytes` configurable ### BUG FIXES: diff --git a/config/config.go b/config/config.go index 32e37f3e1..6a3cb8ce8 100644 --- a/config/config.go +++ b/config/config.go @@ -351,6 +351,12 @@ type RPCConfig struct { // See https://github.com/tendermint/tendermint/issues/3435 TimeoutBroadcastTxCommit time.Duration `mapstructure:"timeout_broadcast_tx_commit"` + // Maximum size of request body, in bytes + MaxBodyBytes int64 `mapstructure:"max_body_bytes"` + + // Maximum size of request header, in bytes + MaxHeaderBytes int `mapstructure:"max_header_bytes"` + // The path to a file containing certificate that is used to create the HTTPS server. // Migth be either absolute path or path related to tendermint's config directory. // @@ -385,6 +391,9 @@ func DefaultRPCConfig() *RPCConfig { MaxSubscriptionsPerClient: 5, TimeoutBroadcastTxCommit: 10 * time.Second, + MaxBodyBytes: int64(1000000), // 1MB + MaxHeaderBytes: 1 << 20, // same as the net/http default + TLSCertFile: "", TLSKeyFile: "", } @@ -417,6 +426,12 @@ func (cfg *RPCConfig) ValidateBasic() error { if cfg.TimeoutBroadcastTxCommit < 0 { return errors.New("timeout_broadcast_tx_commit can't be negative") } + if cfg.MaxBodyBytes < 0 { + return errors.New("max_body_bytes can't be negative") + } + if cfg.MaxHeaderBytes < 0 { + return errors.New("max_header_bytes can't be negative") + } return nil } diff --git a/config/toml.go b/config/toml.go index 09117a0fb..1cafc9c2d 100644 --- a/config/toml.go +++ b/config/toml.go @@ -192,6 +192,12 @@ max_subscriptions_per_client = {{ .RPC.MaxSubscriptionsPerClient }} # See https://github.com/tendermint/tendermint/issues/3435 timeout_broadcast_tx_commit = "{{ .RPC.TimeoutBroadcastTxCommit }}" +# Maximum size of request body, in bytes +max_body_bytes = {{ .RPC.MaxBodyBytes }} + +# Maximum size of request header, in bytes +max_header_bytes = {{ .RPC.MaxHeaderBytes }} + # The path to a file containing certificate that is used to create the HTTPS server. # Migth be either absolute path or path related to tendermint's config directory. # If the certificate is signed by a certificate authority, diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index b9f784596..026a75374 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -138,6 +138,12 @@ max_subscriptions_per_client = 5 # See https://github.com/tendermint/tendermint/issues/3435 timeout_broadcast_tx_commit = "10s" +# Maximum size of request body, in bytes +max_body_bytes = {{ .RPC.MaxBodyBytes }} + +# Maximum size of request header, in bytes +max_header_bytes = {{ .RPC.MaxHeaderBytes }} + # The path to a file containing certificate that is used to create the HTTPS server. # Migth be either absolute path or path related to tendermint's config directory. # If the certificate is signed by a certificate authority, diff --git a/node/node.go b/node/node.go index 9beb0669f..791444b4a 100644 --- a/node/node.go +++ b/node/node.go @@ -820,6 +820,17 @@ func (n *Node) startRPC() ([]net.Listener, error) { rpccore.AddUnsafeRoutes() } + config := rpcserver.DefaultConfig() + config.MaxBodyBytes = n.config.RPC.MaxBodyBytes + config.MaxHeaderBytes = n.config.RPC.MaxHeaderBytes + config.MaxOpenConnections = n.config.RPC.MaxOpenConnections + // If necessary adjust global WriteTimeout to ensure it's greater than + // TimeoutBroadcastTxCommit. + // See https://github.com/tendermint/tendermint/issues/3435 + if config.WriteTimeout <= n.config.RPC.TimeoutBroadcastTxCommit { + config.WriteTimeout = n.config.RPC.TimeoutBroadcastTxCommit + 1*time.Second + } + // we may expose the rpc over both a unix and tcp socket listeners := make([]net.Listener, len(listenAddrs)) for i, listenAddr := range listenAddrs { @@ -832,20 +843,12 @@ func (n *Node) startRPC() ([]net.Listener, error) { if err != nil && err != tmpubsub.ErrSubscriptionNotFound { wmLogger.Error("Failed to unsubscribe addr from events", "addr", remoteAddr, "err", err) } - })) + }), + rpcserver.ReadLimit(config.MaxBodyBytes), + ) wm.SetLogger(wmLogger) mux.HandleFunc("/websocket", wm.WebsocketHandler) rpcserver.RegisterRPCFuncs(mux, rpccore.Routes, coreCodec, rpcLogger) - - config := rpcserver.DefaultConfig() - config.MaxOpenConnections = n.config.RPC.MaxOpenConnections - // If necessary adjust global WriteTimeout to ensure it's greater than - // TimeoutBroadcastTxCommit. - // See https://github.com/tendermint/tendermint/issues/3435 - if config.WriteTimeout <= n.config.RPC.TimeoutBroadcastTxCommit { - config.WriteTimeout = n.config.RPC.TimeoutBroadcastTxCommit + 1*time.Second - } - listener, err := rpcserver.Listen( listenAddr, config, diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index c1c1ebf1a..434ee8916 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -448,6 +448,9 @@ type wsConnection struct { // Send pings to server with this period. Must be less than readWait, but greater than zero. pingPeriod time.Duration + // Maximum message size. + readLimit int64 + // callback which is called upon disconnect onDisconnect func(remoteAddr string) @@ -467,7 +470,6 @@ func NewWSConnection( cdc *amino.Codec, options ...func(*wsConnection), ) *wsConnection { - baseConn.SetReadLimit(maxBodyBytes) wsc := &wsConnection{ remoteAddr: baseConn.RemoteAddr().String(), baseConn: baseConn, @@ -481,6 +483,7 @@ func NewWSConnection( for _, option := range options { option(wsc) } + wsc.baseConn.SetReadLimit(wsc.readLimit) wsc.BaseService = *cmn.NewBaseService(nil, "wsConnection", wsc) return wsc } @@ -525,6 +528,14 @@ func PingPeriod(pingPeriod time.Duration) func(*wsConnection) { } } +// ReadLimit sets the maximum size for reading message. +// It should only be used in the constructor - not Goroutine-safe. +func ReadLimit(readLimit int64) func(*wsConnection) { + return func(wsc *wsConnection) { + wsc.readLimit = readLimit + } +} + // OnStart implements cmn.Service by starting the read and write routines. It // blocks until the connection closes. func (wsc *wsConnection) OnStart() error { diff --git a/rpc/lib/server/http_server.go b/rpc/lib/server/http_server.go index 7825605eb..c97739bd2 100644 --- a/rpc/lib/server/http_server.go +++ b/rpc/lib/server/http_server.go @@ -26,6 +26,11 @@ type Config struct { ReadTimeout time.Duration // mirrors http.Server#WriteTimeout WriteTimeout time.Duration + // MaxBodyBytes controls the maximum number of bytes the + // server will read parsing the request body. + MaxBodyBytes int64 + // mirrors http.Server#MaxHeaderBytes + MaxHeaderBytes int } // DefaultConfig returns a default configuration. @@ -34,28 +39,21 @@ func DefaultConfig() *Config { MaxOpenConnections: 0, // unlimited ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, + MaxBodyBytes: int64(1000000), // 1MB + MaxHeaderBytes: 1 << 20, // same as the net/http default } } -const ( - // maxBodyBytes controls the maximum number of bytes the - // server will read parsing the request body. - maxBodyBytes = int64(1000000) // 1MB - - // same as the net/http default - maxHeaderBytes = 1 << 20 -) - // StartHTTPServer takes a listener and starts an HTTP server with the given handler. // It wraps handler with RecoverAndLogHandler. // NOTE: This function blocks - you may want to call it in a go-routine. func StartHTTPServer(listener net.Listener, handler http.Handler, logger log.Logger, config *Config) error { logger.Info(fmt.Sprintf("Starting RPC HTTP server on %s", listener.Addr())) s := &http.Server{ - Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), + Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger), ReadTimeout: config.ReadTimeout, WriteTimeout: config.WriteTimeout, - MaxHeaderBytes: maxHeaderBytes, + MaxHeaderBytes: config.MaxHeaderBytes, } err := s.Serve(listener) logger.Info("RPC HTTP server stopped", "err", err) @@ -75,10 +73,10 @@ func StartHTTPAndTLSServer( logger.Info(fmt.Sprintf("Starting RPC HTTPS server on %s (cert: %q, key: %q)", listener.Addr(), certFile, keyFile)) s := &http.Server{ - Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: maxBodyBytes}, logger), + Handler: RecoverAndLogHandler(maxBytesHandler{h: handler, n: config.MaxBodyBytes}, logger), ReadTimeout: config.ReadTimeout, WriteTimeout: config.WriteTimeout, - MaxHeaderBytes: maxHeaderBytes, + MaxHeaderBytes: config.MaxHeaderBytes, } err := s.ServeTLS(listener, certFile, keyFile) From ba1f10608674aaf9f3456480479a2fd1cf0c0acf Mon Sep 17 00:00:00 2001 From: zjubfd <296179868@qq.com> Date: Mon, 22 Jul 2019 15:37:41 +0800 Subject: [PATCH 145/211] p2p/conn: Add Bufferpool (#3664) * use byte buffer pool to decreass allocs * wrap to put buffer in defer * wapper defer * add dependency * remove Gopkg,* * add change log --- CHANGELOG_PENDING.md | 1 + go.mod | 1 + go.sum | 2 + p2p/conn/secret_connection.go | 99 +++++++++++++++++------------- p2p/conn/secret_connection_test.go | 61 +++++++++++++++++- 5 files changed, 119 insertions(+), 45 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 416423637..5890c3ce0 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,5 +20,6 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: - [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) - [rpc] \#3818 Make `max_body_bytes` and `max_header_bytes` configurable +- [p2p] \#3664 p2p/conn: reuse buffer when write/read from secret connection ### BUG FIXES: diff --git a/go.mod b/go.mod index 948cf9887..03216b3f5 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmhodges/levigo v1.0.0 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect + github.com/libp2p/go-buffer-pool v0.0.1 github.com/magiconair/properties v1.8.0 github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/mapstructure v1.1.2 // indirect diff --git a/go.sum b/go.sum index 14091bbc0..67a1062c4 100644 --- a/go.sum +++ b/go.sum @@ -60,6 +60,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/libp2p/go-buffer-pool v0.0.1 h1:9Rrn/H46cXjaA2HQ5Y8lyhOS1NhTkZ4yuEs2r3Eechg= +github.com/libp2p/go-buffer-pool v0.0.1/go.mod h1:xtyIz9PMobb13WaxR6Zo1Pd1zXJKYg0a8KiIvDp3TzQ= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= diff --git a/p2p/conn/secret_connection.go b/p2p/conn/secret_connection.go index 7f76ac800..a4489f475 100644 --- a/p2p/conn/secret_connection.go +++ b/p2p/conn/secret_connection.go @@ -2,6 +2,7 @@ package conn import ( "bytes" + "crypto/cipher" crand "crypto/rand" "crypto/sha256" "crypto/subtle" @@ -17,6 +18,7 @@ import ( "golang.org/x/crypto/curve25519" "golang.org/x/crypto/nacl/box" + pool "github.com/libp2p/go-buffer-pool" "github.com/tendermint/tendermint/crypto" cmn "github.com/tendermint/tendermint/libs/common" "golang.org/x/crypto/hkdf" @@ -47,10 +49,11 @@ var ( type SecretConnection struct { // immutable - recvSecret *[aeadKeySize]byte - sendSecret *[aeadKeySize]byte - remPubKey crypto.PubKey - conn io.ReadWriteCloser + recvAead cipher.AEAD + sendAead cipher.AEAD + + remPubKey crypto.PubKey + conn io.ReadWriteCloser // net.Conn must be thread safe: // https://golang.org/pkg/net/#Conn. @@ -102,14 +105,22 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (* // generate the secret used for receiving, sending, challenge via hkdf-sha2 on dhSecret recvSecret, sendSecret, challenge := deriveSecretAndChallenge(dhSecret, locIsLeast) + sendAead, err := chacha20poly1305.New(sendSecret[:]) + if err != nil { + return nil, errors.New("Invalid send SecretConnection Key") + } + recvAead, err := chacha20poly1305.New(recvSecret[:]) + if err != nil { + return nil, errors.New("Invalid receive SecretConnection Key") + } // Construct SecretConnection. sc := &SecretConnection{ conn: conn, recvBuffer: nil, recvNonce: new([aeadNonceSize]byte), sendNonce: new([aeadNonceSize]byte), - recvSecret: recvSecret, - sendSecret: sendSecret, + recvAead: recvAead, + sendAead: sendAead, } // Sign the challenge bytes for authentication. @@ -143,35 +154,39 @@ func (sc *SecretConnection) Write(data []byte) (n int, err error) { defer sc.sendMtx.Unlock() for 0 < len(data) { - var frame = make([]byte, totalFrameSize) - var chunk []byte - if dataMaxSize < len(data) { - chunk = data[:dataMaxSize] - data = data[dataMaxSize:] - } else { - chunk = data - data = nil - } - chunkLength := len(chunk) - binary.LittleEndian.PutUint32(frame, uint32(chunkLength)) - copy(frame[dataLenSize:], chunk) - - aead, err := chacha20poly1305.New(sc.sendSecret[:]) - if err != nil { - return n, errors.New("Invalid SecretConnection Key") - } - - // encrypt the frame - var sealedFrame = make([]byte, aeadSizeOverhead+totalFrameSize) - aead.Seal(sealedFrame[:0], sc.sendNonce[:], frame, nil) - incrNonce(sc.sendNonce) - // end encryption - - _, err = sc.conn.Write(sealedFrame) - if err != nil { + if err := func() error { + var sealedFrame = pool.Get(aeadSizeOverhead + totalFrameSize) + var frame = pool.Get(totalFrameSize) + defer func() { + pool.Put(sealedFrame) + pool.Put(frame) + }() + var chunk []byte + if dataMaxSize < len(data) { + chunk = data[:dataMaxSize] + data = data[dataMaxSize:] + } else { + chunk = data + data = nil + } + chunkLength := len(chunk) + binary.LittleEndian.PutUint32(frame, uint32(chunkLength)) + copy(frame[dataLenSize:], chunk) + + // encrypt the frame + sc.sendAead.Seal(sealedFrame[:0], sc.sendNonce[:], frame, nil) + incrNonce(sc.sendNonce) + // end encryption + + _, err = sc.conn.Write(sealedFrame) + if err != nil { + return err + } + n += len(chunk) + return nil + }(); err != nil { return n, err } - n += len(chunk) } return } @@ -189,21 +204,18 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) { } // read off the conn - sealedFrame := make([]byte, totalFrameSize+aeadSizeOverhead) + var sealedFrame = pool.Get(aeadSizeOverhead + totalFrameSize) + defer pool.Put(sealedFrame) _, err = io.ReadFull(sc.conn, sealedFrame) if err != nil { return } - aead, err := chacha20poly1305.New(sc.recvSecret[:]) - if err != nil { - return n, errors.New("Invalid SecretConnection Key") - } - // decrypt the frame. // reads and updates the sc.recvNonce - var frame = make([]byte, totalFrameSize) - _, err = aead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil) + var frame = pool.Get(totalFrameSize) + defer pool.Put(frame) + _, err = sc.recvAead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil) if err != nil { return n, errors.New("Failed to decrypt SecretConnection") } @@ -218,7 +230,10 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) { } var chunk = frame[dataLenSize : dataLenSize+chunkLength] n = copy(data, chunk) - sc.recvBuffer = chunk[n:] + if n < len(chunk) { + sc.recvBuffer = make([]byte, len(chunk)-n) + copy(sc.recvBuffer, chunk[n:]) + } return } diff --git a/p2p/conn/secret_connection_test.go b/p2p/conn/secret_connection_test.go index 7e264e913..76982ed97 100644 --- a/p2p/conn/secret_connection_test.go +++ b/p2p/conn/secret_connection_test.go @@ -383,10 +383,23 @@ func createGoldenTestVectors(t *testing.T) string { return data } -func BenchmarkSecretConnection(b *testing.B) { +func BenchmarkWriteSecretConnection(b *testing.B) { b.StopTimer() + b.ReportAllocs() fooSecConn, barSecConn := makeSecretConnPair(b) - fooWriteText := cmn.RandStr(dataMaxSize) + randomMsgSizes := []int{ + dataMaxSize / 10, + dataMaxSize / 3, + dataMaxSize / 2, + dataMaxSize, + dataMaxSize * 3 / 2, + dataMaxSize * 2, + dataMaxSize * 7 / 2, + } + fooWriteBytes := make([][]byte, 0, len(randomMsgSizes)) + for _, size := range randomMsgSizes { + fooWriteBytes = append(fooWriteBytes, cmn.RandBytes(size)) + } // Consume reads from bar's reader go func() { readBuffer := make([]byte, dataMaxSize) @@ -402,7 +415,8 @@ func BenchmarkSecretConnection(b *testing.B) { b.StartTimer() for i := 0; i < b.N; i++ { - _, err := fooSecConn.Write([]byte(fooWriteText)) + idx := cmn.RandIntn(len(fooWriteBytes)) + _, err := fooSecConn.Write(fooWriteBytes[idx]) if err != nil { b.Fatalf("Failed to write to fooSecConn: %v", err) } @@ -414,3 +428,44 @@ func BenchmarkSecretConnection(b *testing.B) { } //barSecConn.Close() race condition } + +func BenchmarkReadSecretConnection(b *testing.B) { + b.StopTimer() + b.ReportAllocs() + fooSecConn, barSecConn := makeSecretConnPair(b) + randomMsgSizes := []int{ + dataMaxSize / 10, + dataMaxSize / 3, + dataMaxSize / 2, + dataMaxSize, + dataMaxSize * 3 / 2, + dataMaxSize * 2, + dataMaxSize * 7 / 2, + } + fooWriteBytes := make([][]byte, 0, len(randomMsgSizes)) + for _, size := range randomMsgSizes { + fooWriteBytes = append(fooWriteBytes, cmn.RandBytes(size)) + } + go func() { + for i := 0; i < b.N; i++ { + idx := cmn.RandIntn(len(fooWriteBytes)) + _, err := fooSecConn.Write(fooWriteBytes[idx]) + if err != nil { + b.Fatalf("Failed to write to fooSecConn: %v, %v,%v", err, i, b.N) + } + } + }() + + b.StartTimer() + for i := 0; i < b.N; i++ { + readBuffer := make([]byte, dataMaxSize) + _, err := barSecConn.Read(readBuffer) + + if err == io.EOF { + return + } else if err != nil { + b.Fatalf("Failed to read from barSecConn: %v", err) + } + } + b.StopTimer() +} From 32b7e29bae98dba6c6a5a16b59f07b366b7900f7 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Mon, 22 Jul 2019 12:15:29 +0400 Subject: [PATCH 146/211] rpc: /broadcast_evidence (#3481) * implement broadcast_duplicate_vote endpoint * fix test_cover * address comments * address comments * Update abci/example/kvstore/persistent_kvstore.go Co-Authored-By: mossid * Update rpc/client/main_test.go Co-Authored-By: mossid * address comments in progress * reformat the code * make linter happy * make tests pass * replace BroadcastDuplicateVote with BroadcastEvidence * fix test * fix endpoint name * improve doc * fix TestBroadcastEvidenceDuplicateVote * Update rpc/core/evidence.go Co-Authored-By: Thane Thomson * add changelog entry * fix TestBroadcastEvidenceDuplicateVote --- CHANGELOG_PENDING.md | 4 +- abci/example/kvstore/kvstore.go | 1 + abci/example/kvstore/persistent_kvstore.go | 45 ++++++- rpc/client/amino.go | 12 ++ rpc/client/httpclient.go | 9 ++ rpc/client/interface.go | 52 ++++---- rpc/client/localclient.go | 4 + rpc/client/main_test.go | 7 +- rpc/client/mock/client.go | 5 + rpc/client/rpc_test.go | 144 +++++++++++++++++++++ rpc/core/evidence.go | 39 ++++++ rpc/core/routes.go | 5 +- rpc/core/types/responses.go | 5 + 13 files changed, 301 insertions(+), 31 deletions(-) create mode 100644 rpc/client/amino.go create mode 100644 rpc/core/evidence.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 5890c3ce0..1151d264c 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -18,8 +18,6 @@ program](https://hackerone.com/tendermint). ### FEATURES: ### IMPROVEMENTS: -- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) -- [rpc] \#3818 Make `max_body_bytes` and `max_header_bytes` configurable -- [p2p] \#3664 p2p/conn: reuse buffer when write/read from secret connection + ### BUG FIXES: diff --git a/abci/example/kvstore/kvstore.go b/abci/example/kvstore/kvstore.go index 82d404c76..7e408a534 100644 --- a/abci/example/kvstore/kvstore.go +++ b/abci/example/kvstore/kvstore.go @@ -115,6 +115,7 @@ func (app *KVStoreApplication) Commit() types.ResponseCommit { return types.ResponseCommit{Data: appHash} } +// Returns an associated value or nil if missing. func (app *KVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { if reqQuery.Prove { value := app.state.db.Get(prefixKey(reqQuery.Data)) diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index ba0b53896..f4276b7cd 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -10,7 +10,9 @@ import ( "github.com/tendermint/tendermint/abci/example/code" "github.com/tendermint/tendermint/abci/types" dbm "github.com/tendermint/tendermint/libs/db" + "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/libs/log" + tmtypes "github.com/tendermint/tendermint/types" ) const ( @@ -27,6 +29,8 @@ type PersistentKVStoreApplication struct { // validator set ValUpdates []types.ValidatorUpdate + valAddrToPubKeyMap map[string]types.PubKey + logger log.Logger } @@ -40,8 +44,9 @@ func NewPersistentKVStoreApplication(dbDir string) *PersistentKVStoreApplication state := loadState(db) return &PersistentKVStoreApplication{ - app: &KVStoreApplication{state: state}, - logger: log.NewNopLogger(), + app: &KVStoreApplication{state: state}, + valAddrToPubKeyMap: make(map[string]types.PubKey), + logger: log.NewNopLogger(), } } @@ -83,8 +88,20 @@ func (app *PersistentKVStoreApplication) Commit() types.ResponseCommit { return app.app.Commit() } -func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) types.ResponseQuery { - return app.app.Query(reqQuery) +// When path=/val and data={validator address}, returns the validator update (types.ValidatorUpdate) varint encoded. +// For any other path, returns an associated value or nil if missing. +func (app *PersistentKVStoreApplication) Query(reqQuery types.RequestQuery) (resQuery types.ResponseQuery) { + switch reqQuery.Path { + case "/val": + key := []byte("val:" + string(reqQuery.Data)) + value := app.app.state.db.Get(key) + + resQuery.Key = reqQuery.Data + resQuery.Value = value + return + default: + return app.app.Query(reqQuery) + } } // Save the validators in the merkle tree @@ -102,6 +119,20 @@ func (app *PersistentKVStoreApplication) InitChain(req types.RequestInitChain) t func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) types.ResponseBeginBlock { // reset valset changes app.ValUpdates = make([]types.ValidatorUpdate, 0) + + for _, ev := range req.ByzantineValidators { + switch ev.Type { + case tmtypes.ABCIEvidenceTypeDuplicateVote: + // decrease voting power by 1 + if ev.TotalVotingPower == 0 { + continue + } + app.updateValidator(types.ValidatorUpdate{ + PubKey: app.valAddrToPubKeyMap[string(ev.Validator.Address)], + Power: ev.TotalVotingPower - 1, + }) + } + } return types.ResponseBeginBlock{} } @@ -174,6 +205,10 @@ func (app *PersistentKVStoreApplication) execValidatorTx(tx []byte) types.Respon // add, update, or remove a validator func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate) types.ResponseDeliverTx { key := []byte("val:" + string(v.PubKey.Data)) + + pubkey := ed25519.PubKeyEd25519{} + copy(pubkey[:], v.PubKey.Data) + if v.Power == 0 { // remove validator if !app.app.state.db.Has(key) { @@ -183,6 +218,7 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate Log: fmt.Sprintf("Cannot remove non-existent validator %s", pubStr)} } app.app.state.db.Delete(key) + delete(app.valAddrToPubKeyMap, string(pubkey.Address())) } else { // add or update validator value := bytes.NewBuffer(make([]byte, 0)) @@ -192,6 +228,7 @@ func (app *PersistentKVStoreApplication) updateValidator(v types.ValidatorUpdate Log: fmt.Sprintf("Error encoding validator: %v", err)} } app.app.state.db.Set(key, value.Bytes()) + app.valAddrToPubKeyMap[string(pubkey.Address())] = v.PubKey } // we only update the changes array if we successfully updated the tree diff --git a/rpc/client/amino.go b/rpc/client/amino.go new file mode 100644 index 000000000..ef1a00ec4 --- /dev/null +++ b/rpc/client/amino.go @@ -0,0 +1,12 @@ +package client + +import ( + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/types" +) + +var cdc = amino.NewCodec() + +func init() { + types.RegisterEvidences(cdc) +} diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index 3fd13da37..85f065b61 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -333,6 +333,15 @@ func (c *baseRPCClient) Validators(height *int64) (*ctypes.ResultValidators, err return result, nil } +func (c *baseRPCClient) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + result := new(ctypes.ResultBroadcastEvidence) + _, err := c.caller.Call("broadcast_evidence", map[string]interface{}{"evidence": ev}, result) + if err != nil { + return nil, errors.Wrap(err, "BroadcastEvidence") + } + return result, nil +} + //----------------------------------------------------------------------------- // WSEvents diff --git a/rpc/client/interface.go b/rpc/client/interface.go index 8f9ed9372..383e0b480 100644 --- a/rpc/client/interface.go +++ b/rpc/client/interface.go @@ -28,9 +28,24 @@ import ( "github.com/tendermint/tendermint/types" ) -// ABCIClient groups together the functionality that principally -// affects the ABCI app. In many cases this will be all we want, -// so we can accept an interface which is easier to mock +// Client wraps most important rpc calls a client would make if you want to +// listen for events, test if it also implements events.EventSwitch. +type Client interface { + cmn.Service + ABCIClient + EventsClient + HistoryClient + NetworkClient + SignClient + StatusClient + EvidenceClient +} + +// ABCIClient groups together the functionality that principally affects the +// ABCI app. +// +// In many cases this will be all we want, so we can accept an interface which +// is easier to mock. type ABCIClient interface { // Reading from abci app ABCIInfo() (*ctypes.ResultABCIInfo, error) @@ -44,8 +59,8 @@ type ABCIClient interface { BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) } -// SignClient groups together the interfaces need to get valid -// signatures and prove anything about the chain +// SignClient groups together the functionality needed to get valid signatures +// and prove anything about the chain. type SignClient interface { Block(height *int64) (*ctypes.ResultBlock, error) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) @@ -55,32 +70,19 @@ type SignClient interface { TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) } -// HistoryClient shows us data from genesis to now in large chunks. +// HistoryClient provides access to data from genesis to now in large chunks. type HistoryClient interface { Genesis() (*ctypes.ResultGenesis, error) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) } +// StatusClient provides access to general chain info. type StatusClient interface { - // General chain info Status() (*ctypes.ResultStatus, error) } -// Client wraps most important rpc calls a client would make -// if you want to listen for events, test if it also -// implements events.EventSwitch -type Client interface { - cmn.Service - ABCIClient - EventsClient - HistoryClient - NetworkClient - SignClient - StatusClient -} - -// NetworkClient is general info about the network state. May not -// be needed usually. +// NetworkClient is general info about the network state. May not be needed +// usually. type NetworkClient interface { NetInfo() (*ctypes.ResultNetInfo, error) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) @@ -110,3 +112,9 @@ type MempoolClient interface { UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) } + +// EvidenceClient is used for submitting an evidence of the malicious +// behaviour. +type EvidenceClient interface { + BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) +} diff --git a/rpc/client/localclient.go b/rpc/client/localclient.go index 161f44fdf..3c3a1dcc6 100644 --- a/rpc/client/localclient.go +++ b/rpc/client/localclient.go @@ -157,6 +157,10 @@ func (c *Local) TxSearch(query string, prove bool, page, perPage int) (*ctypes.R return core.TxSearch(c.ctx, query, prove, page, perPage) } +func (c *Local) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + return core.BroadcastEvidence(c.ctx, ev) +} + func (c *Local) Subscribe(ctx context.Context, subscriber, query string, outCapacity ...int) (out <-chan ctypes.ResultEvent, err error) { q, err := tmquery.New(query) if err != nil { diff --git a/rpc/client/main_test.go b/rpc/client/main_test.go index 6ec7b7b0e..d600b32f8 100644 --- a/rpc/client/main_test.go +++ b/rpc/client/main_test.go @@ -1,6 +1,7 @@ package client_test import ( + "io/ioutil" "os" "testing" @@ -13,7 +14,11 @@ var node *nm.Node func TestMain(m *testing.M) { // start a tendermint node (and kvstore) in the background to test against - app := kvstore.NewKVStoreApplication() + dir, err := ioutil.TempDir("/tmp", "rpc-client-test") + if err != nil { + panic(err) + } + app := kvstore.NewPersistentKVStoreApplication(dir) node = rpctest.StartTendermint(app) code := m.Run() diff --git a/rpc/client/mock/client.go b/rpc/client/mock/client.go index c2e19b6d4..3ec40d6cc 100644 --- a/rpc/client/mock/client.go +++ b/rpc/client/mock/client.go @@ -36,6 +36,7 @@ type Client struct { client.HistoryClient client.StatusClient client.EventsClient + client.EvidenceClient cmn.Service } @@ -147,3 +148,7 @@ func (c Client) Commit(height *int64) (*ctypes.ResultCommit, error) { func (c Client) Validators(height *int64) (*ctypes.ResultValidators, error) { return core.Validators(&rpctypes.Context{}, height) } + +func (c Client) BroadcastEvidence(ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + return core.BroadcastEvidence(&rpctypes.Context{}, ev) +} diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index a1a48abc4..de5e18f11 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -1,7 +1,9 @@ package client_test import ( + "bytes" "fmt" + "math/rand" "net/http" "strings" "sync" @@ -12,7 +14,10 @@ import ( abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/tmhash" cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/rpc/client" ctypes "github.com/tendermint/tendermint/rpc/core/types" rpctest "github.com/tendermint/tendermint/rpc/test" @@ -446,6 +451,145 @@ func TestTxSearch(t *testing.T) { } } +func deepcpVote(vote *types.Vote) (res *types.Vote) { + res = &types.Vote{ + ValidatorAddress: make([]byte, len(vote.ValidatorAddress)), + ValidatorIndex: vote.ValidatorIndex, + Height: vote.Height, + Round: vote.Round, + Type: vote.Type, + BlockID: types.BlockID{ + Hash: make([]byte, len(vote.BlockID.Hash)), + PartsHeader: vote.BlockID.PartsHeader, + }, + Signature: make([]byte, len(vote.Signature)), + } + copy(res.ValidatorAddress, vote.ValidatorAddress) + copy(res.BlockID.Hash, vote.BlockID.Hash) + copy(res.Signature, vote.Signature) + return +} + +func newEvidence(t *testing.T, val *privval.FilePV, vote *types.Vote, vote2 *types.Vote, chainID string) types.DuplicateVoteEvidence { + var err error + vote2_ := deepcpVote(vote2) + vote2_.Signature, err = val.Key.PrivKey.Sign(vote2_.SignBytes(chainID)) + require.NoError(t, err) + + return types.DuplicateVoteEvidence{ + PubKey: val.Key.PubKey, + VoteA: vote, + VoteB: vote2_, + } +} + +func makeEvidences(t *testing.T, val *privval.FilePV, chainID string) (ev types.DuplicateVoteEvidence, fakes []types.DuplicateVoteEvidence) { + vote := &types.Vote{ + ValidatorAddress: val.Key.Address, + ValidatorIndex: 0, + Height: 1, + Round: 0, + Type: types.PrevoteType, + BlockID: types.BlockID{ + Hash: tmhash.Sum([]byte("blockhash")), + PartsHeader: types.PartSetHeader{ + Total: 1000, + Hash: tmhash.Sum([]byte("partset")), + }, + }, + } + + var err error + vote.Signature, err = val.Key.PrivKey.Sign(vote.SignBytes(chainID)) + require.NoError(t, err) + + vote2 := deepcpVote(vote) + vote2.BlockID.Hash = tmhash.Sum([]byte("blockhash2")) + + ev = newEvidence(t, val, vote, vote2, chainID) + + fakes = make([]types.DuplicateVoteEvidence, 42) + + // different address + vote2 = deepcpVote(vote) + for i := 0; i < 10; i++ { + rand.Read(vote2.ValidatorAddress) // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different index + vote2 = deepcpVote(vote) + for i := 10; i < 20; i++ { + vote2.ValidatorIndex = rand.Int()%100 + 1 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different height + vote2 = deepcpVote(vote) + for i := 20; i < 30; i++ { + vote2.Height = rand.Int63()%1000 + 100 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different round + vote2 = deepcpVote(vote) + for i := 30; i < 40; i++ { + vote2.Round = rand.Int()%10 + 1 // nolint: gosec + fakes[i] = newEvidence(t, val, vote, vote2, chainID) + } + // different type + vote2 = deepcpVote(vote) + vote2.Type = types.PrecommitType + fakes[40] = newEvidence(t, val, vote, vote2, chainID) + // exactly same vote + vote2 = deepcpVote(vote) + fakes[41] = newEvidence(t, val, vote, vote2, chainID) + return +} + +func TestBroadcastEvidenceDuplicateVote(t *testing.T) { + config := rpctest.GetConfig() + chainID := config.ChainID() + pvKeyFile := config.PrivValidatorKeyFile() + pvKeyStateFile := config.PrivValidatorStateFile() + pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) + + ev, fakes := makeEvidences(t, pv, chainID) + + t.Logf("evidence %v", ev) + + for i, c := range GetClients() { + t.Logf("client %d", i) + + result, err := c.BroadcastEvidence(&types.DuplicateVoteEvidence{PubKey: ev.PubKey, VoteA: ev.VoteA, VoteB: ev.VoteB}) + require.Nil(t, err) + require.Equal(t, ev.Hash(), result.Hash, "Invalid response, result %+v", result) + + status, err := c.Status() + require.NoError(t, err) + client.WaitForHeight(c, status.SyncInfo.LatestBlockHeight+2, nil) + + ed25519pub := ev.PubKey.(ed25519.PubKeyEd25519) + rawpub := ed25519pub[:] + result2, err := c.ABCIQuery("/val", rawpub) + require.Nil(t, err, "Error querying evidence, err %v", err) + qres := result2.Response + require.True(t, qres.IsOK(), "Response not OK") + + var v abci.ValidatorUpdate + err = abci.ReadMessage(bytes.NewReader(qres.Value), &v) + require.NoError(t, err, "Error reading query result, value %v", qres.Value) + + require.EqualValues(t, rawpub, v.PubKey.Data, "Stored PubKey not equal with expected, value %v", string(qres.Value)) + require.Equal(t, int64(9), v.Power, "Stored Power not equal with expected, value %v", string(qres.Value)) + + for _, fake := range fakes { + _, err := c.BroadcastEvidence(&types.DuplicateVoteEvidence{ + PubKey: fake.PubKey, + VoteA: fake.VoteA, + VoteB: fake.VoteB}) + require.Error(t, err, "Broadcasting fake evidence succeed: %s", fake.String()) + } + } +} + func TestBatchedJSONRPCCalls(t *testing.T) { c := getHTTPClient() testBatchedJSONRPCCalls(t, c) diff --git a/rpc/core/evidence.go b/rpc/core/evidence.go new file mode 100644 index 000000000..b2dfd097f --- /dev/null +++ b/rpc/core/evidence.go @@ -0,0 +1,39 @@ +package core + +import ( + ctypes "github.com/tendermint/tendermint/rpc/core/types" + rpctypes "github.com/tendermint/tendermint/rpc/lib/types" + "github.com/tendermint/tendermint/types" +) + +// Broadcast evidence of the misbehavior. +// +// ```shell +// curl 'localhost:26657/broadcast_evidence?evidence={amino-encoded DuplicateVoteEvidence}' +// ``` +// +// ```go +// client := client.NewHTTP("tcp://0.0.0.0:26657", "/websocket") +// err := client.Start() +// if err != nil { +// // handle error +// } +// defer client.Stop() +// res, err := client.BroadcastEvidence(&types.DuplicateVoteEvidence{PubKey: ev.PubKey, VoteA: ev.VoteA, VoteB: ev.VoteB}) +// ``` +// +// > The above command returns JSON structured like this: +// +// ```json +// ``` +// +// | Parameter | Type | Default | Required | Description | +// |-----------+----------------+---------+----------+-----------------------------| +// | evidence | types.Evidence | nil | true | Amino-encoded JSON evidence | +func BroadcastEvidence(ctx *rpctypes.Context, ev types.Evidence) (*ctypes.ResultBroadcastEvidence, error) { + err := evidencePool.AddEvidence(ev) + if err != nil { + return nil, err + } + return &ctypes.ResultBroadcastEvidence{Hash: ev.Hash()}, nil +} diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 736ded607..df7cef905 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -30,7 +30,7 @@ var Routes = map[string]*rpc.RPCFunc{ "unconfirmed_txs": rpc.NewRPCFunc(UnconfirmedTxs, "limit"), "num_unconfirmed_txs": rpc.NewRPCFunc(NumUnconfirmedTxs, ""), - // broadcast API + // tx broadcast API "broadcast_tx_commit": rpc.NewRPCFunc(BroadcastTxCommit, "tx"), "broadcast_tx_sync": rpc.NewRPCFunc(BroadcastTxSync, "tx"), "broadcast_tx_async": rpc.NewRPCFunc(BroadcastTxAsync, "tx"), @@ -38,6 +38,9 @@ var Routes = map[string]*rpc.RPCFunc{ // abci API "abci_query": rpc.NewRPCFunc(ABCIQuery, "path,data,height,prove"), "abci_info": rpc.NewRPCFunc(ABCIInfo, ""), + + // evidence API + "broadcast_evidence": rpc.NewRPCFunc(BroadcastEvidence, "evidence"), } func AddUnsafeRoutes() { diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index f1ae16a39..f8a9476f3 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -194,6 +194,11 @@ type ResultABCIQuery struct { Response abci.ResponseQuery `json:"response"` } +// Result of broadcasting evidence +type ResultBroadcastEvidence struct { + Hash []byte `json:"hash"` +} + // empty results type ( ResultUnsafeFlushMempool struct{} From f59956ced9a4e40049ec4a0c2d52a17327f8e836 Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Tue, 23 Jul 2019 00:17:10 +0900 Subject: [PATCH 147/211] mempool: make max_msg_bytes configurable (#3826) * mempool: make max_msg_bytes configurable * apply suggestions from code review * update changelog pending * apply suggestions from code review again --- CHANGELOG_PENDING.md | 1 - config/config.go | 5 +++++ config/toml.go | 3 +++ docs/tendermint-core/configuration.md | 3 +++ mempool/clist_mempool.go | 7 ++++--- mempool/clist_mempool_test.go | 5 ++++- mempool/errors.go | 13 ++++++++++--- mempool/reactor.go | 17 +++++++++++------ 8 files changed, 40 insertions(+), 14 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 1151d264c..fd5c4b27e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -19,5 +19,4 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: - ### BUG FIXES: diff --git a/config/config.go b/config/config.go index 6a3cb8ce8..73c704681 100644 --- a/config/config.go +++ b/config/config.go @@ -631,6 +631,7 @@ type MempoolConfig struct { Size int `mapstructure:"size"` MaxTxsBytes int64 `mapstructure:"max_txs_bytes"` CacheSize int `mapstructure:"cache_size"` + MaxMsgBytes int `mapstructure:"max_msg_bytes"` } // DefaultMempoolConfig returns a default configuration for the Tendermint mempool @@ -644,6 +645,7 @@ func DefaultMempoolConfig() *MempoolConfig { Size: 5000, MaxTxsBytes: 1024 * 1024 * 1024, // 1GB CacheSize: 10000, + MaxMsgBytes: 1024 * 1024, // 1MB } } @@ -676,6 +678,9 @@ func (cfg *MempoolConfig) ValidateBasic() error { if cfg.CacheSize < 0 { return errors.New("cache_size can't be negative") } + if cfg.MaxMsgBytes < 0 { + return errors.New("max_msg_bytes can't be negative") + } return nil } diff --git a/config/toml.go b/config/toml.go index 1cafc9c2d..58da57975 100644 --- a/config/toml.go +++ b/config/toml.go @@ -294,6 +294,9 @@ max_txs_bytes = {{ .Mempool.MaxTxsBytes }} # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = {{ .Mempool.CacheSize }} +# Limit the size of TxMessage +max_msg_bytes = {{ .Mempool.MaxMsgBytes }} + ##### consensus configuration options ##### [consensus] diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index 026a75374..ff502a222 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -240,6 +240,9 @@ max_txs_bytes = 1073741824 # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = 10000 +# Limit the size of TxMessage +max_msg_bytes = 1048576 + ##### consensus configuration options ##### [consensus] diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 4042e9b4b..81123cb63 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -220,9 +220,10 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t var ( memSize = mem.Size() txsBytes = mem.TxsBytes() + txSize = len(tx) ) if memSize >= mem.config.Size || - int64(len(tx))+txsBytes > mem.config.MaxTxsBytes { + int64(txSize)+txsBytes > mem.config.MaxTxsBytes { return ErrMempoolIsFull{ memSize, mem.config.Size, txsBytes, mem.config.MaxTxsBytes} @@ -231,8 +232,8 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t // The size of the corresponding amino-encoded TxMessage // can't be larger than the maxMsgSize, otherwise we can't // relay it to peers. - if len(tx) > maxTxSize { - return ErrTxTooLarge + if max := calcMaxTxSize(mem.config.MaxMsgBytes); txSize > max { + return ErrTxTooLarge{max, txSize} } if mem.preCheck != nil { diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index 90d0ed1ae..e2ebca925 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -426,6 +426,9 @@ func TestMempoolMaxMsgSize(t *testing.T) { mempl, cleanup := newMempoolWithApp(cc) defer cleanup() + maxMsgSize := mempl.config.MaxMsgBytes + maxTxSize := calcMaxTxSize(mempl.config.MaxMsgBytes) + testCases := []struct { len int err bool @@ -462,7 +465,7 @@ func TestMempoolMaxMsgSize(t *testing.T) { require.NoError(t, err, caseString) } else { require.True(t, len(encoded) > maxMsgSize, caseString) - require.Equal(t, err, ErrTxTooLarge, caseString) + require.Equal(t, err, ErrTxTooLarge{maxTxSize, testCase.len}, caseString) } } diff --git a/mempool/errors.go b/mempool/errors.go index ac2a9b3c2..c5140bdf0 100644 --- a/mempool/errors.go +++ b/mempool/errors.go @@ -9,11 +9,18 @@ import ( var ( // ErrTxInCache is returned to the client if we saw tx earlier ErrTxInCache = errors.New("Tx already exists in cache") - - // ErrTxTooLarge means the tx is too big to be sent in a message to other peers - ErrTxTooLarge = fmt.Errorf("Tx too large. Max size is %d", maxTxSize) ) +// ErrTxTooLarge means the tx is too big to be sent in a message to other peers +type ErrTxTooLarge struct { + max int + actual int +} + +func (e ErrTxTooLarge) Error() string { + return fmt.Sprintf("Tx too large. Max size is %d, but got %d", e.max, e.actual) +} + // ErrMempoolIsFull means Tendermint & an application can't handle that much load type ErrMempoolIsFull struct { numTxs int diff --git a/mempool/reactor.go b/mempool/reactor.go index 65ccd7dfd..0ca273401 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -19,8 +19,7 @@ import ( const ( MempoolChannel = byte(0x30) - maxMsgSize = 1048576 // 1MB TODO make it configurable - maxTxSize = maxMsgSize - 8 // account for amino overhead of TxMessage + aminoOverheadForTxMessage = 8 peerCatchupSleepIntervalMS = 100 // If peer is behind, sleep this amount @@ -156,7 +155,7 @@ func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { // Receive implements Reactor. // It adds any received transactions to the mempool. func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := decodeMsg(msgBytes) + msg, err := memR.decodeMsg(msgBytes) if err != nil { memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) memR.Switch.StopPeerForError(src, err) @@ -263,9 +262,9 @@ func RegisterMempoolMessages(cdc *amino.Codec) { cdc.RegisterConcrete(&TxMessage{}, "tendermint/mempool/TxMessage", nil) } -func decodeMsg(bz []byte) (msg MempoolMessage, err error) { - if len(bz) > maxMsgSize { - return msg, fmt.Errorf("Msg exceeds max size (%d > %d)", len(bz), maxMsgSize) +func (memR *Reactor) decodeMsg(bz []byte) (msg MempoolMessage, err error) { + if l := len(bz); l > memR.config.MaxMsgBytes { + return msg, ErrTxTooLarge{memR.config.MaxMsgBytes, l} } err = cdc.UnmarshalBinaryBare(bz, &msg) return @@ -282,3 +281,9 @@ type TxMessage struct { func (m *TxMessage) String() string { return fmt.Sprintf("[TxMessage %v]", m.Tx) } + +// calcMaxTxSize returns the max size of Tx +// account for amino overhead of TxMessage +func calcMaxTxSize(maxMsgSize int) int { + return maxMsgSize - aminoOverheadForTxMessage +} From e6202d1673114c0a069fa3ba6497c139e388d7ec Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 23 Jul 2019 12:25:59 +0400 Subject: [PATCH 148/211] =?UTF-8?q?rpc:=20return=20err=20if=20page=20is=20?= =?UTF-8?q?incorrect=20(less=20than=200=20or=20greater=20than=20tot?= =?UTF-8?q?=E2=80=A6=20(#3825)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * rpc: return err if page is incorrect (less than 0 or greater than total pages) Fixes #3813 * fix rpc_test --- CHANGELOG_PENDING.md | 2 ++ rpc/core/pipe.go | 20 +++++++++++++------- rpc/core/pipe_test.go | 42 ++++++++++++++++++++++++------------------ rpc/core/tx.go | 5 ++++- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index fd5c4b27e..13019bc69 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,3 +20,5 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: ### BUG FIXES: + +- [rpc] \#3813 Return err if page is incorrect (less than 0 or greater than total pages) diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 28a492e6f..e733c4efd 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -1,6 +1,7 @@ package core import ( + "fmt" "time" cfg "github.com/tendermint/tendermint/config" @@ -145,19 +146,24 @@ func SetConfig(c cfg.RPCConfig) { config = c } -func validatePage(page, perPage, totalCount int) int { +func validatePage(page, perPage, totalCount int) (int, error) { if perPage < 1 { - return 1 + panic(fmt.Sprintf("zero or negative perPage: %d", perPage)) + } + + if page == 0 { + return 1, nil // default } pages := ((totalCount - 1) / perPage) + 1 - if page < 1 { - page = 1 - } else if page > pages { - page = pages + if pages == 0 { + pages = 1 // one page (even if it's empty) + } + if page < 0 || page > pages { + return 1, fmt.Errorf("page should be within [0, %d] range, given %d", pages, page) } - return page + return page, nil } func validatePerPage(perPage int) int { diff --git a/rpc/core/pipe_test.go b/rpc/core/pipe_test.go index 19ed11fcc..93aff3e58 100644 --- a/rpc/core/pipe_test.go +++ b/rpc/core/pipe_test.go @@ -14,33 +14,39 @@ func TestPaginationPage(t *testing.T) { perPage int page int newPage int + expErr bool }{ - {0, 0, 1, 1}, + {0, 10, 1, 1, false}, - {0, 10, 0, 1}, - {0, 10, 1, 1}, - {0, 10, 2, 1}, + {0, 10, 0, 1, false}, + {0, 10, 1, 1, false}, + {0, 10, 2, 0, true}, - {5, 10, -1, 1}, - {5, 10, 0, 1}, - {5, 10, 1, 1}, - {5, 10, 2, 1}, - {5, 10, 2, 1}, + {5, 10, -1, 0, true}, + {5, 10, 0, 1, false}, + {5, 10, 1, 1, false}, + {5, 10, 2, 0, true}, + {5, 10, 2, 0, true}, - {5, 5, 1, 1}, - {5, 5, 2, 1}, - {5, 5, 3, 1}, + {5, 5, 1, 1, false}, + {5, 5, 2, 0, true}, + {5, 5, 3, 0, true}, - {5, 3, 2, 2}, - {5, 3, 3, 2}, + {5, 3, 2, 2, false}, + {5, 3, 3, 0, true}, - {5, 2, 2, 2}, - {5, 2, 3, 3}, - {5, 2, 4, 3}, + {5, 2, 2, 2, false}, + {5, 2, 3, 3, false}, + {5, 2, 4, 0, true}, } for _, c := range cases { - p := validatePage(c.page, c.perPage, c.totalCount) + p, err := validatePage(c.page, c.perPage, c.totalCount) + if c.expErr { + assert.Error(t, err) + continue + } + assert.Equal(t, c.newPage, p, fmt.Sprintf("%v", c)) } diff --git a/rpc/core/tx.go b/rpc/core/tx.go index 575553f85..dba457c30 100644 --- a/rpc/core/tx.go +++ b/rpc/core/tx.go @@ -202,7 +202,10 @@ func TxSearch(ctx *rpctypes.Context, query string, prove bool, page, perPage int totalCount := len(results) perPage = validatePerPage(perPage) - page = validatePage(page, perPage, totalCount) + page, err = validatePage(page, perPage, totalCount) + if err != nil { + return nil, err + } skipCount := validateSkipCount(page, perPage) apiResults := make([]*ctypes.ResultTx, cmn.MinInt(perPage, totalCount-skipCount)) From 1de39b4f658fcf9b85bbbcd3c5e036b17fa423f6 Mon Sep 17 00:00:00 2001 From: Anca Zamfir Date: Tue, 23 Jul 2019 10:58:52 +0200 Subject: [PATCH 149/211] blockchain: Reorg reactor (#3561) * go routines in blockchain reactor * Added reference to the go routine diagram * Initial commit * cleanup * Undo testing_logger change, committed by mistake * Fix the test loggers * pulled some fsm code into pool.go * added pool tests * changes to the design added block requests under peer moved the request trigger in the reactor poolRoutine, triggered now by a ticker in general moved everything required for making block requests smarter in the poolRoutine added a simple map of heights to keep track of what will need to be requested next added a few more tests * send errors to FSM in a different channel than blocks send errors (RemovePeer) from switch on a different channel than the one receiving blocks renamed channels added more pool tests * more pool tests * lint errors * more tests * more tests * switch fast sync to new implementation * fixed data race in tests * cleanup * finished fsm tests * address golangci comments :) * address golangci comments :) * Added timeout on next block needed to advance * updating docs and cleanup * fix issue in test from previous cleanup * cleanup * Added termination scenarios, tests and more cleanup * small fixes to adr, comments and cleanup * Fix bug in sendRequest() If we tried to send a request to a peer not present in the switch, a missing continue statement caused the request to be blackholed in a peer that was removed and never retried. While this bug was manifesting, the reactor kept asking for other blocks that would be stored and never consumed. Added the number of unconsumed blocks in the math for requesting blocks ahead of current processing height so eventually there will be no more blocks requested until the already received ones are consumed. * remove bpPeer's didTimeout field * Use distinct err codes for peer timeout and FSM timeouts * Don't allow peers to update with lower height * review comments from Ethan and Zarko * some cleanup, renaming, comments * Move block execution in separate goroutine * Remove pool's numPending * review comments * fix lint, remove old blockchain reactor and duplicates in fsm tests * small reorg around peer after review comments * add the reactor spec * verify block only once * review comments * change to int for max number of pending requests * cleanup and godoc * Add configuration flag fast sync version * golangci fixes * fix config template * move both reactor versions under blockchain * cleanup, golint, renaming stuff * updated documentation, fixed more golint warnings * integrate with behavior package * sync with master * gofmt * add changelog_pending entry * move to improvments * suggestion to changelog entry --- CHANGELOG_PENDING.md | 2 - blockchain/{ => v0}/pool.go | 17 +- blockchain/{ => v0}/pool_test.go | 2 +- blockchain/{ => v0}/reactor.go | 12 +- blockchain/{ => v0}/reactor_test.go | 6 +- blockchain/{ => v0}/wire.go | 2 +- blockchain/v1/peer.go | 209 ++++ blockchain/v1/peer_test.go | 278 ++++++ blockchain/v1/pool.go | 369 +++++++ blockchain/v1/pool_test.go | 650 ++++++++++++ blockchain/v1/reactor.go | 620 ++++++++++++ blockchain/v1/reactor_fsm.go | 450 +++++++++ blockchain/v1/reactor_fsm_test.go | 938 ++++++++++++++++++ blockchain/v1/reactor_test.go | 337 +++++++ blockchain/v1/wire.go | 13 + cmd/tendermint/commands/run_node.go | 2 +- config/config.go | 44 +- config/toml.go | 10 +- consensus/common_test.go | 4 +- consensus/reactor_test.go | 4 +- consensus/replay_file.go | 4 +- consensus/wal_generator.go | 5 +- .../bcv1/img/bc-reactor-new-datastructs.png | Bin 0 -> 44461 bytes .../bcv1/img/bc-reactor-new-fsm.png | Bin 0 -> 42091 bytes .../bcv1/img/bc-reactor-new-goroutines.png | Bin 0 -> 140946 bytes docs/spec/reactors/block_sync/bcv1/impl-v1.md | 237 +++++ .../block_sync/img/bc-reactor-routines.png | Bin 0 -> 271695 bytes docs/spec/reactors/block_sync/impl.md | 13 +- docs/tendermint-core/configuration.md | 8 + go.sum | 25 +- node/node.go | 60 +- {blockchain => store}/store.go | 3 +- {blockchain => store}/store_test.go | 14 +- store/wire.go | 12 + 34 files changed, 4296 insertions(+), 54 deletions(-) rename blockchain/{ => v0}/pool.go (96%) rename blockchain/{ => v0}/pool_test.go (99%) rename blockchain/{ => v0}/reactor.go (97%) rename blockchain/{ => v0}/reactor_test.go (98%) rename blockchain/{ => v0}/wire.go (91%) create mode 100644 blockchain/v1/peer.go create mode 100644 blockchain/v1/peer_test.go create mode 100644 blockchain/v1/pool.go create mode 100644 blockchain/v1/pool_test.go create mode 100644 blockchain/v1/reactor.go create mode 100644 blockchain/v1/reactor_fsm.go create mode 100644 blockchain/v1/reactor_fsm_test.go create mode 100644 blockchain/v1/reactor_test.go create mode 100644 blockchain/v1/wire.go create mode 100644 docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-datastructs.png create mode 100644 docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-fsm.png create mode 100644 docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-goroutines.png create mode 100644 docs/spec/reactors/block_sync/bcv1/impl-v1.md create mode 100644 docs/spec/reactors/block_sync/img/bc-reactor-routines.png rename {blockchain => store}/store.go (98%) rename {blockchain => store}/store_test.go (97%) create mode 100644 store/wire.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 13019bc69..fd5c4b27e 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,5 +20,3 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: ### BUG FIXES: - -- [rpc] \#3813 Return err if page is incorrect (less than 0 or greater than total pages) diff --git a/blockchain/pool.go b/blockchain/v0/pool.go similarity index 96% rename from blockchain/pool.go rename to blockchain/v0/pool.go index c842c0d13..bd570f216 100644 --- a/blockchain/pool.go +++ b/blockchain/v0/pool.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "errors" @@ -59,6 +59,7 @@ var peerTimeout = 15 * time.Second // not const so we can override with tests are not at peer limits, we can probably switch to consensus reactor */ +// BlockPool keeps track of the fast sync peers, block requests and block responses. type BlockPool struct { cmn.BaseService startTime time.Time @@ -184,6 +185,7 @@ func (pool *BlockPool) IsCaughtUp() bool { return isCaughtUp } +// PeekTwoBlocks returns blocks at pool.height and pool.height+1. // We need to see the second block's Commit to validate the first block. // So we peek two blocks at a time. // The caller will verify the commit. @@ -200,7 +202,7 @@ func (pool *BlockPool) PeekTwoBlocks() (first *types.Block, second *types.Block) return } -// Pop the first block at pool.height +// PopRequest pops the first block at pool.height. // It must have been validated by 'second'.Commit from PeekTwoBlocks(). func (pool *BlockPool) PopRequest() { pool.mtx.Lock() @@ -220,7 +222,7 @@ func (pool *BlockPool) PopRequest() { } } -// Invalidates the block at pool.height, +// RedoRequest invalidates the block at pool.height, // Remove the peer and redo request from others. // Returns the ID of the removed peer. func (pool *BlockPool) RedoRequest(height int64) p2p.ID { @@ -236,6 +238,7 @@ func (pool *BlockPool) RedoRequest(height int64) p2p.ID { return peerID } +// AddBlock validates that the block comes from the peer it was expected from and calls the requester to store it. // TODO: ensure that blocks come in order for each peer. func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) { pool.mtx.Lock() @@ -565,9 +568,9 @@ func (bpr *bpRequester) reset() { // Tells bpRequester to pick another peer and try again. // NOTE: Nonblocking, and does nothing if another redo // was already requested. -func (bpr *bpRequester) redo(peerId p2p.ID) { +func (bpr *bpRequester) redo(peerID p2p.ID) { select { - case bpr.redoCh <- peerId: + case bpr.redoCh <- peerID: default: } } @@ -622,8 +625,8 @@ OUTER_LOOP: } } -//------------------------------------- - +// BlockRequest stores a block request identified by the block Height and the PeerID responsible for +// delivering the block type BlockRequest struct { Height int64 PeerID p2p.ID diff --git a/blockchain/pool_test.go b/blockchain/v0/pool_test.go similarity index 99% rename from blockchain/pool_test.go rename to blockchain/v0/pool_test.go index 01d7dba20..d741d59df 100644 --- a/blockchain/pool_test.go +++ b/blockchain/v0/pool_test.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "fmt" diff --git a/blockchain/reactor.go b/blockchain/v0/reactor.go similarity index 97% rename from blockchain/reactor.go rename to blockchain/v0/reactor.go index 139393778..5d38471dc 100644 --- a/blockchain/reactor.go +++ b/blockchain/v0/reactor.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "errors" @@ -11,6 +11,7 @@ import ( "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) @@ -60,7 +61,7 @@ type BlockchainReactor struct { initialState sm.State blockExec *sm.BlockExecutor - store *BlockStore + store *store.BlockStore pool *BlockPool fastSync bool @@ -69,7 +70,7 @@ type BlockchainReactor struct { } // NewBlockchainReactor returns new reactor instance. -func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *BlockStore, +func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, fastSync bool) *BlockchainReactor { if state.LastBlockHeight != store.Height() { @@ -378,6 +379,7 @@ type BlockchainMessage interface { ValidateBasic() error } +// RegisterBlockchainMessages registers the fast sync messages for amino encoding. func RegisterBlockchainMessages(cdc *amino.Codec) { cdc.RegisterInterface((*BlockchainMessage)(nil), nil) cdc.RegisterConcrete(&bcBlockRequestMessage{}, "tendermint/blockchain/BlockRequest", nil) @@ -425,8 +427,8 @@ func (m *bcNoBlockResponseMessage) ValidateBasic() error { return nil } -func (brm *bcNoBlockResponseMessage) String() string { - return fmt.Sprintf("[bcNoBlockResponseMessage %d]", brm.Height) +func (m *bcNoBlockResponseMessage) String() string { + return fmt.Sprintf("[bcNoBlockResponseMessage %d]", m.Height) } //------------------------------------- diff --git a/blockchain/reactor_test.go b/blockchain/v0/reactor_test.go similarity index 98% rename from blockchain/reactor_test.go rename to blockchain/v0/reactor_test.go index b5137bb2a..2000b134d 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/v0/reactor_test.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( "os" @@ -6,6 +6,8 @@ import ( "testing" "time" + "github.com/tendermint/tendermint/store" + "github.com/stretchr/testify/assert" abci "github.com/tendermint/tendermint/abci/types" @@ -81,7 +83,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals blockDB := dbm.NewMemDB() stateDB := dbm.NewMemDB() - blockStore := NewBlockStore(blockDB) + blockStore := store.NewBlockStore(blockDB) state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) if err != nil { diff --git a/blockchain/wire.go b/blockchain/v0/wire.go similarity index 91% rename from blockchain/wire.go rename to blockchain/v0/wire.go index 487fbe2bc..4494f41aa 100644 --- a/blockchain/wire.go +++ b/blockchain/v0/wire.go @@ -1,4 +1,4 @@ -package blockchain +package v0 import ( amino "github.com/tendermint/go-amino" diff --git a/blockchain/v1/peer.go b/blockchain/v1/peer.go new file mode 100644 index 000000000..02b1b4fc1 --- /dev/null +++ b/blockchain/v1/peer.go @@ -0,0 +1,209 @@ +package v1 + +import ( + "fmt" + "math" + "time" + + flow "github.com/tendermint/tendermint/libs/flowrate" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +//-------- +// Peer + +// BpPeerParams stores the peer parameters that are used when creating a peer. +type BpPeerParams struct { + timeout time.Duration + minRecvRate int64 + sampleRate time.Duration + windowSize time.Duration +} + +// BpPeer is the datastructure associated with a fast sync peer. +type BpPeer struct { + logger log.Logger + ID p2p.ID + + Height int64 // the peer reported height + NumPendingBlockRequests int // number of requests still waiting for block responses + blocks map[int64]*types.Block // blocks received or expected to be received from this peer + blockResponseTimer *time.Timer + recvMonitor *flow.Monitor + params *BpPeerParams // parameters for timer and monitor + + onErr func(err error, peerID p2p.ID) // function to call on error +} + +// NewBpPeer creates a new peer. +func NewBpPeer( + peerID p2p.ID, height int64, onErr func(err error, peerID p2p.ID), params *BpPeerParams) *BpPeer { + + if params == nil { + params = BpPeerDefaultParams() + } + return &BpPeer{ + ID: peerID, + Height: height, + blocks: make(map[int64]*types.Block, maxRequestsPerPeer), + logger: log.NewNopLogger(), + onErr: onErr, + params: params, + } +} + +// String returns a string representation of a peer. +func (peer *BpPeer) String() string { + return fmt.Sprintf("peer: %v height: %v pending: %v", peer.ID, peer.Height, peer.NumPendingBlockRequests) +} + +// SetLogger sets the logger of the peer. +func (peer *BpPeer) SetLogger(l log.Logger) { + peer.logger = l +} + +// Cleanup performs cleanup of the peer, removes blocks, requests, stops timer and monitor. +func (peer *BpPeer) Cleanup() { + if peer.blockResponseTimer != nil { + peer.blockResponseTimer.Stop() + } + if peer.NumPendingBlockRequests != 0 { + peer.logger.Info("peer with pending requests is being cleaned", "peer", peer.ID) + } + if len(peer.blocks)-peer.NumPendingBlockRequests != 0 { + peer.logger.Info("peer with pending blocks is being cleaned", "peer", peer.ID) + } + for h := range peer.blocks { + delete(peer.blocks, h) + } + peer.NumPendingBlockRequests = 0 + peer.recvMonitor = nil +} + +// BlockAtHeight returns the block at a given height if available and errMissingBlock otherwise. +func (peer *BpPeer) BlockAtHeight(height int64) (*types.Block, error) { + block, ok := peer.blocks[height] + if !ok { + return nil, errMissingBlock + } + if block == nil { + return nil, errMissingBlock + } + return peer.blocks[height], nil +} + +// AddBlock adds a block at peer level. Block must be non-nil and recvSize a positive integer +// The peer must have a pending request for this block. +func (peer *BpPeer) AddBlock(block *types.Block, recvSize int) error { + if block == nil || recvSize < 0 { + panic("bad parameters") + } + existingBlock, ok := peer.blocks[block.Height] + if !ok { + peer.logger.Error("unsolicited block", "blockHeight", block.Height, "peer", peer.ID) + return errMissingBlock + } + if existingBlock != nil { + peer.logger.Error("already have a block for height", "height", block.Height) + return errDuplicateBlock + } + if peer.NumPendingBlockRequests == 0 { + panic("peer does not have pending requests") + } + peer.blocks[block.Height] = block + peer.NumPendingBlockRequests-- + if peer.NumPendingBlockRequests == 0 { + peer.stopMonitor() + peer.stopBlockResponseTimer() + } else { + peer.recvMonitor.Update(recvSize) + peer.resetBlockResponseTimer() + } + return nil +} + +// RemoveBlock removes the block of given height +func (peer *BpPeer) RemoveBlock(height int64) { + delete(peer.blocks, height) +} + +// RequestSent records that a request was sent, and starts the peer timer and monitor if needed. +func (peer *BpPeer) RequestSent(height int64) { + peer.blocks[height] = nil + + if peer.NumPendingBlockRequests == 0 { + peer.startMonitor() + peer.resetBlockResponseTimer() + } + peer.NumPendingBlockRequests++ +} + +// CheckRate verifies that the response rate of the peer is acceptable (higher than the minimum allowed). +func (peer *BpPeer) CheckRate() error { + if peer.NumPendingBlockRequests == 0 { + return nil + } + curRate := peer.recvMonitor.Status().CurRate + // curRate can be 0 on start + if curRate != 0 && curRate < peer.params.minRecvRate { + err := errSlowPeer + peer.logger.Error("SendTimeout", "peer", peer, + "reason", err, + "curRate", fmt.Sprintf("%d KB/s", curRate/1024), + "minRate", fmt.Sprintf("%d KB/s", peer.params.minRecvRate/1024)) + return err + } + return nil +} + +func (peer *BpPeer) onTimeout() { + peer.onErr(errNoPeerResponse, peer.ID) +} + +func (peer *BpPeer) stopMonitor() { + peer.recvMonitor.Done() + peer.recvMonitor = nil +} + +func (peer *BpPeer) startMonitor() { + peer.recvMonitor = flow.New(peer.params.sampleRate, peer.params.windowSize) + initialValue := float64(peer.params.minRecvRate) * math.E + peer.recvMonitor.SetREMA(initialValue) +} + +func (peer *BpPeer) resetBlockResponseTimer() { + if peer.blockResponseTimer == nil { + peer.blockResponseTimer = time.AfterFunc(peer.params.timeout, peer.onTimeout) + } else { + peer.blockResponseTimer.Reset(peer.params.timeout) + } +} + +func (peer *BpPeer) stopBlockResponseTimer() bool { + if peer.blockResponseTimer == nil { + return false + } + return peer.blockResponseTimer.Stop() +} + +// BpPeerDefaultParams returns the default peer parameters. +func BpPeerDefaultParams() *BpPeerParams { + return &BpPeerParams{ + // Timeout for a peer to respond to a block request. + timeout: 15 * time.Second, + + // Minimum recv rate to ensure we're receiving blocks from a peer fast + // enough. If a peer is not sending data at at least that rate, we + // consider them to have timedout and we disconnect. + // + // Assuming a DSL connection (not a good choice) 128 Kbps (upload) ~ 15 KB/s, + // sending data across atlantic ~ 7.5 KB/s. + minRecvRate: int64(7680), + + // Monitor parameters + sampleRate: time.Second, + windowSize: 40 * time.Second, + } +} diff --git a/blockchain/v1/peer_test.go b/blockchain/v1/peer_test.go new file mode 100644 index 000000000..3c19e4efd --- /dev/null +++ b/blockchain/v1/peer_test.go @@ -0,0 +1,278 @@ +package v1 + +import ( + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +func TestPeerMonitor(t *testing.T) { + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + nil) + peer.SetLogger(log.TestingLogger()) + peer.startMonitor() + assert.NotNil(t, peer.recvMonitor) + peer.stopMonitor() + assert.Nil(t, peer.recvMonitor) +} + +func TestPeerResetBlockResponseTimer(t *testing.T) { + var ( + numErrFuncCalls int // number of calls to the errFunc + lastErr error // last generated error + peerTestMtx sync.Mutex // modifications of ^^ variables are also done from timer handler goroutine + ) + params := &BpPeerParams{timeout: 2 * time.Millisecond} + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) { + peerTestMtx.Lock() + defer peerTestMtx.Unlock() + lastErr = err + numErrFuncCalls++ + }, + params) + + peer.SetLogger(log.TestingLogger()) + checkByStoppingPeerTimer(t, peer, false) + + // initial reset call with peer having a nil timer + peer.resetBlockResponseTimer() + assert.NotNil(t, peer.blockResponseTimer) + // make sure timer is running and stop it + checkByStoppingPeerTimer(t, peer, true) + + // reset with running timer + peer.resetBlockResponseTimer() + time.Sleep(time.Millisecond) + peer.resetBlockResponseTimer() + assert.NotNil(t, peer.blockResponseTimer) + + // let the timer expire and ... + time.Sleep(3 * time.Millisecond) + // ... check timer is not running + checkByStoppingPeerTimer(t, peer, false) + + peerTestMtx.Lock() + // ... check errNoPeerResponse has been sent + assert.Equal(t, 1, numErrFuncCalls) + assert.Equal(t, lastErr, errNoPeerResponse) + peerTestMtx.Unlock() +} + +func TestPeerRequestSent(t *testing.T) { + params := &BpPeerParams{timeout: 2 * time.Millisecond} + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + params) + + peer.SetLogger(log.TestingLogger()) + + peer.RequestSent(1) + assert.NotNil(t, peer.recvMonitor) + assert.NotNil(t, peer.blockResponseTimer) + assert.Equal(t, 1, peer.NumPendingBlockRequests) + + peer.RequestSent(1) + assert.NotNil(t, peer.recvMonitor) + assert.NotNil(t, peer.blockResponseTimer) + assert.Equal(t, 2, peer.NumPendingBlockRequests) +} + +func TestPeerGetAndRemoveBlock(t *testing.T) { + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 100, + func(err error, _ p2p.ID) {}, + nil) + + // Change peer height + peer.Height = int64(10) + assert.Equal(t, int64(10), peer.Height) + + // request some blocks and receive few of them + for i := 1; i <= 10; i++ { + peer.RequestSent(int64(i)) + if i > 5 { + // only receive blocks 1..5 + continue + } + _ = peer.AddBlock(makeSmallBlock(i), 10) + } + + tests := []struct { + name string + height int64 + wantErr error + blockPresent bool + }{ + {"no request", 100, errMissingBlock, false}, + {"no block", 6, errMissingBlock, false}, + {"block 1 present", 1, nil, true}, + {"block max present", 5, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // try to get the block + b, err := peer.BlockAtHeight(tt.height) + assert.Equal(t, tt.wantErr, err) + assert.Equal(t, tt.blockPresent, b != nil) + + // remove the block + peer.RemoveBlock(tt.height) + _, err = peer.BlockAtHeight(tt.height) + assert.Equal(t, errMissingBlock, err) + }) + } +} + +func TestPeerAddBlock(t *testing.T) { + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 100, + func(err error, _ p2p.ID) {}, + nil) + + // request some blocks, receive one + for i := 1; i <= 10; i++ { + peer.RequestSent(int64(i)) + if i == 5 { + // receive block 5 + _ = peer.AddBlock(makeSmallBlock(i), 10) + } + } + + tests := []struct { + name string + height int64 + wantErr error + blockPresent bool + }{ + {"no request", 50, errMissingBlock, false}, + {"duplicate block", 5, errDuplicateBlock, true}, + {"block 1 successfully received", 1, nil, true}, + {"block max successfully received", 10, nil, true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // try to get the block + err := peer.AddBlock(makeSmallBlock(int(tt.height)), 10) + assert.Equal(t, tt.wantErr, err) + _, err = peer.BlockAtHeight(tt.height) + assert.Equal(t, tt.blockPresent, err == nil) + }) + } +} + +func TestPeerOnErrFuncCalledDueToExpiration(t *testing.T) { + + params := &BpPeerParams{timeout: 2 * time.Millisecond} + var ( + numErrFuncCalls int // number of calls to the onErr function + lastErr error // last generated error + peerTestMtx sync.Mutex // modifications of ^^ variables are also done from timer handler goroutine + ) + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) { + peerTestMtx.Lock() + defer peerTestMtx.Unlock() + lastErr = err + numErrFuncCalls++ + }, + params) + + peer.SetLogger(log.TestingLogger()) + + peer.RequestSent(1) + time.Sleep(4 * time.Millisecond) + // timer should have expired by now, check that the on error function was called + peerTestMtx.Lock() + assert.Equal(t, 1, numErrFuncCalls) + assert.Equal(t, errNoPeerResponse, lastErr) + peerTestMtx.Unlock() +} + +func TestPeerCheckRate(t *testing.T) { + params := &BpPeerParams{ + timeout: time.Second, + minRecvRate: int64(100), // 100 bytes/sec exponential moving average + } + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + params) + peer.SetLogger(log.TestingLogger()) + + require.Nil(t, peer.CheckRate()) + + for i := 0; i < 40; i++ { + peer.RequestSent(int64(i)) + } + + // monitor starts with a higher rEMA (~ 2*minRecvRate), wait for it to go down + time.Sleep(900 * time.Millisecond) + + // normal peer - send a bit more than 100 bytes/sec, > 10 bytes/100msec, check peer is not considered slow + for i := 0; i < 10; i++ { + _ = peer.AddBlock(makeSmallBlock(i), 11) + time.Sleep(100 * time.Millisecond) + require.Nil(t, peer.CheckRate()) + } + + // slow peer - send a bit less than 10 bytes/100msec + for i := 10; i < 20; i++ { + _ = peer.AddBlock(makeSmallBlock(i), 9) + time.Sleep(100 * time.Millisecond) + } + // check peer is considered slow + assert.Equal(t, errSlowPeer, peer.CheckRate()) +} + +func TestPeerCleanup(t *testing.T) { + params := &BpPeerParams{timeout: 2 * time.Millisecond} + + peer := NewBpPeer( + p2p.ID(cmn.RandStr(12)), 10, + func(err error, _ p2p.ID) {}, + params) + peer.SetLogger(log.TestingLogger()) + + assert.Nil(t, peer.blockResponseTimer) + peer.RequestSent(1) + assert.NotNil(t, peer.blockResponseTimer) + + peer.Cleanup() + checkByStoppingPeerTimer(t, peer, false) +} + +// Check if peer timer is running or not (a running timer can be successfully stopped). +// Note: stops the timer. +func checkByStoppingPeerTimer(t *testing.T, peer *BpPeer, running bool) { + assert.NotPanics(t, func() { + stopped := peer.stopBlockResponseTimer() + if running { + assert.True(t, stopped) + } else { + assert.False(t, stopped) + } + }) +} + +func makeSmallBlock(height int) *types.Block { + return types.MakeBlock(int64(height), []types.Tx{types.Tx("foo")}, nil, nil) +} diff --git a/blockchain/v1/pool.go b/blockchain/v1/pool.go new file mode 100644 index 000000000..5de741305 --- /dev/null +++ b/blockchain/v1/pool.go @@ -0,0 +1,369 @@ +package v1 + +import ( + "sort" + + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +// BlockPool keeps track of the fast sync peers, block requests and block responses. +type BlockPool struct { + logger log.Logger + // Set of peers that have sent status responses, with height bigger than pool.Height + peers map[p2p.ID]*BpPeer + // Set of block heights and the corresponding peers from where a block response is expected or has been received. + blocks map[int64]p2p.ID + + plannedRequests map[int64]struct{} // list of blocks to be assigned peers for blockRequest + nextRequestHeight int64 // next height to be added to plannedRequests + + Height int64 // height of next block to execute + MaxPeerHeight int64 // maximum height of all peers + toBcR bcReactor +} + +// NewBlockPool creates a new BlockPool. +func NewBlockPool(height int64, toBcR bcReactor) *BlockPool { + return &BlockPool{ + Height: height, + MaxPeerHeight: 0, + peers: make(map[p2p.ID]*BpPeer), + blocks: make(map[int64]p2p.ID), + plannedRequests: make(map[int64]struct{}), + nextRequestHeight: height, + toBcR: toBcR, + } +} + +// SetLogger sets the logger of the pool. +func (pool *BlockPool) SetLogger(l log.Logger) { + pool.logger = l +} + +// ReachedMaxHeight check if the pool has reached the maximum peer height. +func (pool *BlockPool) ReachedMaxHeight() bool { + return pool.Height >= pool.MaxPeerHeight +} + +func (pool *BlockPool) rescheduleRequest(peerID p2p.ID, height int64) { + pool.logger.Info("reschedule requests made to peer for height ", "peerID", peerID, "height", height) + pool.plannedRequests[height] = struct{}{} + delete(pool.blocks, height) + pool.peers[peerID].RemoveBlock(height) +} + +// Updates the pool's max height. If no peers are left MaxPeerHeight is set to 0. +func (pool *BlockPool) updateMaxPeerHeight() { + var newMax int64 + for _, peer := range pool.peers { + peerHeight := peer.Height + if peerHeight > newMax { + newMax = peerHeight + } + } + pool.MaxPeerHeight = newMax +} + +// UpdatePeer adds a new peer or updates an existing peer with a new height. +// If a peer is short it is not added. +func (pool *BlockPool) UpdatePeer(peerID p2p.ID, height int64) error { + + peer := pool.peers[peerID] + + if peer == nil { + if height < pool.Height { + pool.logger.Info("Peer height too small", + "peer", peerID, "height", height, "fsm_height", pool.Height) + return errPeerTooShort + } + // Add new peer. + peer = NewBpPeer(peerID, height, pool.toBcR.sendPeerError, nil) + peer.SetLogger(pool.logger.With("peer", peerID)) + pool.peers[peerID] = peer + pool.logger.Info("added peer", "peerID", peerID, "height", height, "num_peers", len(pool.peers)) + } else { + // Check if peer is lowering its height. This is not allowed. + if height < peer.Height { + pool.RemovePeer(peerID, errPeerLowersItsHeight) + return errPeerLowersItsHeight + } + // Update existing peer. + peer.Height = height + } + + // Update the pool's MaxPeerHeight if needed. + pool.updateMaxPeerHeight() + + return nil +} + +// Cleans and deletes the peer. Recomputes the max peer height. +func (pool *BlockPool) deletePeer(peer *BpPeer) { + if peer == nil { + return + } + peer.Cleanup() + delete(pool.peers, peer.ID) + + if peer.Height == pool.MaxPeerHeight { + pool.updateMaxPeerHeight() + } +} + +// RemovePeer removes the blocks and requests from the peer, reschedules them and deletes the peer. +func (pool *BlockPool) RemovePeer(peerID p2p.ID, err error) { + peer := pool.peers[peerID] + if peer == nil { + return + } + pool.logger.Info("removing peer", "peerID", peerID, "error", err) + + // Reschedule the block requests made to the peer, or received and not processed yet. + // Note that some of the requests may be removed further down. + for h := range pool.peers[peerID].blocks { + pool.rescheduleRequest(peerID, h) + } + + oldMaxPeerHeight := pool.MaxPeerHeight + // Delete the peer. This operation may result in the pool's MaxPeerHeight being lowered. + pool.deletePeer(peer) + + // Check if the pool's MaxPeerHeight has been lowered. + // This may happen if the tallest peer has been removed. + if oldMaxPeerHeight > pool.MaxPeerHeight { + // Remove any planned requests for heights over the new MaxPeerHeight. + for h := range pool.plannedRequests { + if h > pool.MaxPeerHeight { + delete(pool.plannedRequests, h) + } + } + // Adjust the nextRequestHeight to the new max plus one. + if pool.nextRequestHeight > pool.MaxPeerHeight { + pool.nextRequestHeight = pool.MaxPeerHeight + 1 + } + } +} + +func (pool *BlockPool) removeShortPeers() { + for _, peer := range pool.peers { + if peer.Height < pool.Height { + pool.RemovePeer(peer.ID, nil) + } + } +} + +func (pool *BlockPool) removeBadPeers() { + pool.removeShortPeers() + for _, peer := range pool.peers { + if err := peer.CheckRate(); err != nil { + pool.RemovePeer(peer.ID, err) + pool.toBcR.sendPeerError(err, peer.ID) + } + } +} + +// MakeNextRequests creates more requests if the block pool is running low. +func (pool *BlockPool) MakeNextRequests(maxNumRequests int) { + heights := pool.makeRequestBatch(maxNumRequests) + if len(heights) != 0 { + pool.logger.Info("makeNextRequests will make following requests", + "number", len(heights), "heights", heights) + } + + for _, height := range heights { + h := int64(height) + if !pool.sendRequest(h) { + // If a good peer was not found for sending the request at height h then return, + // as it shouldn't be possible to find a peer for h+1. + return + } + delete(pool.plannedRequests, h) + } +} + +// Makes a batch of requests sorted by height such that the block pool has up to maxNumRequests entries. +func (pool *BlockPool) makeRequestBatch(maxNumRequests int) []int { + pool.removeBadPeers() + // At this point pool.requests may include heights for requests to be redone due to removal of peers: + // - peers timed out or were removed by switch + // - FSM timed out on waiting to advance the block execution due to missing blocks at h or h+1 + // Determine the number of requests needed by subtracting the number of requests already made from the maximum + // allowed + numNeeded := int(maxNumRequests) - len(pool.blocks) + for len(pool.plannedRequests) < numNeeded { + if pool.nextRequestHeight > pool.MaxPeerHeight { + break + } + pool.plannedRequests[pool.nextRequestHeight] = struct{}{} + pool.nextRequestHeight++ + } + + heights := make([]int, 0, len(pool.plannedRequests)) + for k := range pool.plannedRequests { + heights = append(heights, int(k)) + } + sort.Ints(heights) + return heights +} + +func (pool *BlockPool) sendRequest(height int64) bool { + for _, peer := range pool.peers { + if peer.NumPendingBlockRequests >= maxRequestsPerPeer { + continue + } + if peer.Height < height { + continue + } + + err := pool.toBcR.sendBlockRequest(peer.ID, height) + if err == errNilPeerForBlockRequest { + // Switch does not have this peer, remove it and continue to look for another peer. + pool.logger.Error("switch does not have peer..removing peer selected for height", "peer", + peer.ID, "height", height) + pool.RemovePeer(peer.ID, err) + continue + } + + if err == errSendQueueFull { + pool.logger.Error("peer queue is full", "peer", peer.ID, "height", height) + continue + } + + pool.logger.Info("assigned request to peer", "peer", peer.ID, "height", height) + + pool.blocks[height] = peer.ID + peer.RequestSent(height) + + return true + } + pool.logger.Error("could not find peer to send request for block at height", "height", height) + return false +} + +// AddBlock validates that the block comes from the peer it was expected from and stores it in the 'blocks' map. +func (pool *BlockPool) AddBlock(peerID p2p.ID, block *types.Block, blockSize int) error { + peer, ok := pool.peers[peerID] + if !ok { + pool.logger.Error("block from unknown peer", "height", block.Height, "peer", peerID) + return errBadDataFromPeer + } + if wantPeerID, ok := pool.blocks[block.Height]; ok && wantPeerID != peerID { + pool.logger.Error("block received from wrong peer", "height", block.Height, + "peer", peerID, "expected_peer", wantPeerID) + return errBadDataFromPeer + } + + return peer.AddBlock(block, blockSize) +} + +// BlockData stores the peer responsible to deliver a block and the actual block if delivered. +type BlockData struct { + block *types.Block + peer *BpPeer +} + +// BlockAndPeerAtHeight retrieves the block and delivery peer at specified height. +// Returns errMissingBlock if a block was not found +func (pool *BlockPool) BlockAndPeerAtHeight(height int64) (bData *BlockData, err error) { + peerID := pool.blocks[height] + peer := pool.peers[peerID] + if peer == nil { + return nil, errMissingBlock + } + + block, err := peer.BlockAtHeight(height) + if err != nil { + return nil, err + } + + return &BlockData{peer: peer, block: block}, nil + +} + +// FirstTwoBlocksAndPeers returns the blocks and the delivery peers at pool's height H and H+1. +func (pool *BlockPool) FirstTwoBlocksAndPeers() (first, second *BlockData, err error) { + first, err = pool.BlockAndPeerAtHeight(pool.Height) + second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1) + if err == nil { + err = err2 + } + return +} + +// InvalidateFirstTwoBlocks removes the peers that sent us the first two blocks, blocks are removed by RemovePeer(). +func (pool *BlockPool) InvalidateFirstTwoBlocks(err error) { + first, err1 := pool.BlockAndPeerAtHeight(pool.Height) + second, err2 := pool.BlockAndPeerAtHeight(pool.Height + 1) + + if err1 == nil { + pool.RemovePeer(first.peer.ID, err) + } + if err2 == nil { + pool.RemovePeer(second.peer.ID, err) + } +} + +// ProcessedCurrentHeightBlock performs cleanup after a block is processed. It removes block at pool height and +// the peers that are now short. +func (pool *BlockPool) ProcessedCurrentHeightBlock() { + peerID, peerOk := pool.blocks[pool.Height] + if peerOk { + pool.peers[peerID].RemoveBlock(pool.Height) + } + delete(pool.blocks, pool.Height) + pool.logger.Debug("removed block at height", "height", pool.Height) + pool.Height++ + pool.removeShortPeers() +} + +// RemovePeerAtCurrentHeights checks if a block at pool's height H exists and if not, it removes the +// delivery peer and returns. If a block at height H exists then the check and peer removal is done for H+1. +// This function is called when the FSM is not able to make progress for some time. +// This happens if either the block H or H+1 have not been delivered. +func (pool *BlockPool) RemovePeerAtCurrentHeights(err error) { + peerID := pool.blocks[pool.Height] + peer, ok := pool.peers[peerID] + if ok { + if _, err := peer.BlockAtHeight(pool.Height); err != nil { + pool.logger.Info("remove peer that hasn't sent block at pool.Height", + "peer", peerID, "height", pool.Height) + pool.RemovePeer(peerID, err) + return + } + } + peerID = pool.blocks[pool.Height+1] + peer, ok = pool.peers[peerID] + if ok { + if _, err := peer.BlockAtHeight(pool.Height + 1); err != nil { + pool.logger.Info("remove peer that hasn't sent block at pool.Height+1", + "peer", peerID, "height", pool.Height+1) + pool.RemovePeer(peerID, err) + return + } + } +} + +// Cleanup performs pool and peer cleanup +func (pool *BlockPool) Cleanup() { + for id, peer := range pool.peers { + peer.Cleanup() + delete(pool.peers, id) + } + pool.plannedRequests = make(map[int64]struct{}) + pool.blocks = make(map[int64]p2p.ID) + pool.nextRequestHeight = 0 + pool.Height = 0 + pool.MaxPeerHeight = 0 +} + +// NumPeers returns the number of peers in the pool +func (pool *BlockPool) NumPeers() int { + return len(pool.peers) +} + +// NeedsBlocks returns true if more blocks are required. +func (pool *BlockPool) NeedsBlocks() bool { + return len(pool.blocks) < maxNumRequests +} diff --git a/blockchain/v1/pool_test.go b/blockchain/v1/pool_test.go new file mode 100644 index 000000000..72758d3b1 --- /dev/null +++ b/blockchain/v1/pool_test.go @@ -0,0 +1,650 @@ +package v1 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +type testPeer struct { + id p2p.ID + height int64 +} + +type testBcR struct { + logger log.Logger +} + +type testValues struct { + numRequestsSent int +} + +var testResults testValues + +func resetPoolTestResults() { + testResults.numRequestsSent = 0 +} + +func (testR *testBcR) sendPeerError(err error, peerID p2p.ID) { +} + +func (testR *testBcR) sendStatusRequest() { +} + +func (testR *testBcR) sendBlockRequest(peerID p2p.ID, height int64) error { + testResults.numRequestsSent++ + return nil +} + +func (testR *testBcR) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { +} + +func (testR *testBcR) switchToConsensus() { + +} + +func newTestBcR() *testBcR { + testBcR := &testBcR{logger: log.TestingLogger()} + return testBcR +} + +type tPBlocks struct { + id p2p.ID + create bool +} + +// Makes a block pool with specified current height, list of peers, block requests and block responses +func makeBlockPool(bcr *testBcR, height int64, peers []BpPeer, blocks map[int64]tPBlocks) *BlockPool { + bPool := NewBlockPool(height, bcr) + bPool.SetLogger(bcr.logger) + + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + + var maxH int64 + for _, p := range peers { + if p.Height > maxH { + maxH = p.Height + } + bPool.peers[p.ID] = NewBpPeer(p.ID, p.Height, bcr.sendPeerError, nil) + bPool.peers[p.ID].SetLogger(bcr.logger) + + } + bPool.MaxPeerHeight = maxH + for h, p := range blocks { + bPool.blocks[h] = p.id + bPool.peers[p.id].RequestSent(int64(h)) + if p.create { + // simulate that a block at height h has been received + _ = bPool.peers[p.id].AddBlock(types.MakeBlock(int64(h), txs, nil, nil), 100) + } + } + return bPool +} + +func assertPeerSetsEquivalent(t *testing.T, set1 map[p2p.ID]*BpPeer, set2 map[p2p.ID]*BpPeer) { + assert.Equal(t, len(set1), len(set2)) + for peerID, peer1 := range set1 { + peer2 := set2[peerID] + assert.NotNil(t, peer2) + assert.Equal(t, peer1.NumPendingBlockRequests, peer2.NumPendingBlockRequests) + assert.Equal(t, peer1.Height, peer2.Height) + assert.Equal(t, len(peer1.blocks), len(peer2.blocks)) + for h, block1 := range peer1.blocks { + block2 := peer2.blocks[h] + // block1 and block2 could be nil if a request was made but no block was received + assert.Equal(t, block1, block2) + } + } +} + +func assertBlockPoolEquivalent(t *testing.T, poolWanted, pool *BlockPool) { + assert.Equal(t, poolWanted.blocks, pool.blocks) + assertPeerSetsEquivalent(t, poolWanted.peers, pool.peers) + assert.Equal(t, poolWanted.MaxPeerHeight, pool.MaxPeerHeight) + assert.Equal(t, poolWanted.Height, pool.Height) + +} + +func TestBlockPoolUpdatePeer(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + args testPeer + poolWanted *BlockPool + errWanted error + }{ + { + name: "add a first short peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + args: testPeer{"P1", 50}, + errWanted: errPeerTooShort, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "add a first good peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + args: testPeer{"P1", 101}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 101}}, map[int64]tPBlocks{}), + }, + { + name: "increase the height of P1 from 120 to 123", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: testPeer{"P1", 123}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 123}}, map[int64]tPBlocks{}), + }, + { + name: "decrease the height of P1 from 120 to 110", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: testPeer{"P1", 110}, + errWanted: errPeerLowersItsHeight, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "decrease the height of P1 from 105 to 102 with blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 105}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 101: {"P1", true}, 102: {"P1", true}}), + args: testPeer{"P1", 102}, + errWanted: errPeerLowersItsHeight, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, + map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pool := tt.pool + err := pool.UpdatePeer(tt.args.id, tt.args.height) + assert.Equal(t, tt.errWanted, err) + assert.Equal(t, tt.poolWanted.blocks, tt.pool.blocks) + assertPeerSetsEquivalent(t, tt.poolWanted.peers, tt.pool.peers) + assert.Equal(t, tt.poolWanted.MaxPeerHeight, tt.pool.MaxPeerHeight) + }) + } +} + +func TestBlockPoolRemovePeer(t *testing.T) { + testBcR := newTestBcR() + + type args struct { + peerID p2p.ID + err error + } + + tests := []struct { + name string + pool *BlockPool + args args + poolWanted *BlockPool + }{ + { + name: "attempt to delete non-existing peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P99", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + }, + { + name: "delete the only peer without blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "delete the shortest of two peers without blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 120}}, map[int64]tPBlocks{}), + }, + { + name: "delete the tallest of two peers without blocks", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 120}}, map[int64]tPBlocks{}), + args: args{"P2", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + }, + { + name: "delete the only peer with block requests sent and blocks received", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "delete the shortest of two peers with block requests sent and blocks received", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 200}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 200}}, map[int64]tPBlocks{}), + }, + { + name: "delete the tallest of two peers with block requests sent and blocks received", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 110}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + args: args{"P1", nil}, + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P2", Height: 110}}, map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.RemovePeer(tt.args.peerID, tt.args.err) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestBlockPoolRemoveShortPeers(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "no short peers", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 110}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 110}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + }, + + { + name: "one short peer", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 90}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P3", Height: 120}}, map[int64]tPBlocks{}), + }, + + { + name: "all short peers", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 90}, {ID: "P2", Height: 91}, {ID: "P3", Height: 92}}, map[int64]tPBlocks{}), + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pool := tt.pool + pool.removeShortPeers() + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestBlockPoolSendRequestBatch(t *testing.T) { + type testPeerResult struct { + id p2p.ID + numPendingBlockRequests int + } + + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + maxRequestsPerPeer int + expRequests map[int64]bool + expPeerResults []testPeerResult + expnumPendingBlockRequests int + }{ + { + name: "one peer - send up to maxRequestsPerPeer block requests", + pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + maxRequestsPerPeer: 2, + expRequests: map[int64]bool{10: true, 11: true}, + expPeerResults: []testPeerResult{{id: "P1", numPendingBlockRequests: 2}}, + expnumPendingBlockRequests: 2, + }, + { + name: "n peers - send n*maxRequestsPerPeer block requests", + pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, map[int64]tPBlocks{}), + maxRequestsPerPeer: 2, + expRequests: map[int64]bool{10: true, 11: true}, + expPeerResults: []testPeerResult{ + {id: "P1", numPendingBlockRequests: 2}, + {id: "P2", numPendingBlockRequests: 2}}, + expnumPendingBlockRequests: 4, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resetPoolTestResults() + + var pool = tt.pool + maxRequestsPerPeer = tt.maxRequestsPerPeer + pool.MakeNextRequests(10) + assert.Equal(t, testResults.numRequestsSent, maxRequestsPerPeer*len(pool.peers)) + + for _, tPeer := range tt.expPeerResults { + var peer = pool.peers[tPeer.id] + assert.NotNil(t, peer) + assert.Equal(t, tPeer.numPendingBlockRequests, peer.NumPendingBlockRequests) + } + assert.Equal(t, testResults.numRequestsSent, maxRequestsPerPeer*len(pool.peers)) + + }) + } +} + +func TestBlockPoolAddBlock(t *testing.T) { + testBcR := newTestBcR() + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + + type args struct { + peerID p2p.ID + block *types.Block + blockSize int + } + tests := []struct { + name string + pool *BlockPool + args args + poolWanted *BlockPool + errWanted error + }{ + {name: "block from unknown peer", + pool: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + args: args{ + peerID: "P2", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, []BpPeer{{ID: "P1", Height: 100}}, map[int64]tPBlocks{}), + errWanted: errBadDataFromPeer, + }, + {name: "unexpected block 11 from known peer - waiting for 10", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + args: args{ + peerID: "P1", + block: types.MakeBlock(int64(11), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + errWanted: errMissingBlock, + }, + {name: "unexpected block 10 from known peer - already have 10", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P1", false}}), + args: args{ + peerID: "P1", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P1", false}}), + errWanted: errDuplicateBlock, + }, + {name: "unexpected block 10 from known peer P2 - expected 10 to come from P1", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + args: args{ + peerID: "P2", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + errWanted: errBadDataFromPeer, + }, + {name: "expected block from known peer", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", false}}), + args: args{ + peerID: "P1", + block: types.MakeBlock(int64(10), txs, nil, nil), + blockSize: 100, + }, + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}}), + errWanted: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.pool.AddBlock(tt.args.peerID, tt.args.block, tt.args.blockSize) + assert.Equal(t, tt.errWanted, err) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestBlockPoolFirstTwoBlocksAndPeers(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + firstWanted int64 + secondWanted int64 + errWanted error + }{ + { + name: "both blocks missing", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), + errWanted: errMissingBlock, + }, + { + name: "second block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 18: {"P2", true}}), + firstWanted: 15, + errWanted: errMissingBlock, + }, + { + name: "first block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{16: {"P2", true}, 18: {"P2", true}}), + secondWanted: 16, + errWanted: errMissingBlock, + }, + { + name: "both blocks present", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P2", true}}), + firstWanted: 10, + secondWanted: 11, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pool := tt.pool + gotFirst, gotSecond, err := pool.FirstTwoBlocksAndPeers() + assert.Equal(t, tt.errWanted, err) + + if tt.firstWanted != 0 { + peer := pool.blocks[tt.firstWanted] + block := pool.peers[peer].blocks[tt.firstWanted] + assert.Equal(t, block, gotFirst.block, + "BlockPool.FirstTwoBlocksAndPeers() gotFirst = %v, want %v", + tt.firstWanted, gotFirst.block.Height) + } + + if tt.secondWanted != 0 { + peer := pool.blocks[tt.secondWanted] + block := pool.peers[peer].blocks[tt.secondWanted] + assert.Equal(t, block, gotSecond.block, + "BlockPool.FirstTwoBlocksAndPeers() gotFirst = %v, want %v", + tt.secondWanted, gotSecond.block.Height) + } + }) + } +} + +func TestBlockPoolInvalidateFirstTwoBlocks(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "both blocks missing", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 16: {"P2", true}}), + }, + { + name: "second block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{15: {"P1", true}, 18: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P2", Height: 100}}, + map[int64]tPBlocks{18: {"P2", true}}), + }, + { + name: "first block missing", + pool: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{18: {"P1", true}, 16: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 15, + []BpPeer{{ID: "P1", Height: 100}}, + map[int64]tPBlocks{18: {"P1", true}}), + }, + { + name: "both blocks present", + pool: makeBlockPool(testBcR, 10, + []BpPeer{{ID: "P1", Height: 100}, {ID: "P2", Height: 100}}, + map[int64]tPBlocks{10: {"P1", true}, 11: {"P2", true}}), + poolWanted: makeBlockPool(testBcR, 10, + []BpPeer{}, + map[int64]tPBlocks{}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.InvalidateFirstTwoBlocks(errNoPeerResponse) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestProcessedCurrentHeightBlock(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "one peer", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", true}}), + poolWanted: makeBlockPool(testBcR, 101, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{101: {"P1", true}}), + }, + { + name: "multiple peers", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + poolWanted: makeBlockPool(testBcR, 101, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.ProcessedCurrentHeightBlock() + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} + +func TestRemovePeerAtCurrentHeight(t *testing.T) { + testBcR := newTestBcR() + + tests := []struct { + name string + pool *BlockPool + poolWanted *BlockPool + }{ + { + name: "one peer, remove peer for block at H", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", false}, 101: {"P1", true}}), + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "one peer, remove peer for block at H+1", + pool: makeBlockPool(testBcR, 100, []BpPeer{{ID: "P1", Height: 120}}, + map[int64]tPBlocks{100: {"P1", true}, 101: {"P1", false}}), + poolWanted: makeBlockPool(testBcR, 100, []BpPeer{}, map[int64]tPBlocks{}), + }, + { + name: "multiple peers, remove peer for block at H", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", false}, 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 101: {"P2", true}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + }, + { + name: "multiple peers, remove peer for block at H+1", + pool: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P2", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, + 101: {"P2", false}, 103: {"P2", false}, + 102: {"P3", true}, 106: {"P3", true}}), + poolWanted: makeBlockPool(testBcR, 100, + []BpPeer{{ID: "P1", Height: 120}, {ID: "P3", Height: 130}}, + map[int64]tPBlocks{ + 100: {"P1", true}, 104: {"P1", true}, 105: {"P1", false}, + 102: {"P3", true}, 106: {"P3", true}}), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tt.pool.RemovePeerAtCurrentHeights(errNoPeerResponse) + assertBlockPoolEquivalent(t, tt.poolWanted, tt.pool) + }) + } +} diff --git a/blockchain/v1/reactor.go b/blockchain/v1/reactor.go new file mode 100644 index 000000000..2f95cebaf --- /dev/null +++ b/blockchain/v1/reactor.go @@ -0,0 +1,620 @@ +package v1 + +import ( + "errors" + "fmt" + "reflect" + "time" + + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/behaviour" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" + "github.com/tendermint/tendermint/types" +) + +const ( + // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) + BlockchainChannel = byte(0x40) + trySyncIntervalMS = 10 + trySendIntervalMS = 10 + + // ask for best height every 10s + statusUpdateIntervalSeconds = 10 + + // NOTE: keep up to date with bcBlockResponseMessage + bcBlockResponseMessagePrefixSize = 4 + bcBlockResponseMessageFieldKeySize = 1 + maxMsgSize = types.MaxBlockSizeBytes + + bcBlockResponseMessagePrefixSize + + bcBlockResponseMessageFieldKeySize +) + +var ( + // Maximum number of requests that can be pending per peer, i.e. for which requests have been sent but blocks + // have not been received. + maxRequestsPerPeer = 20 + // Maximum number of block requests for the reactor, pending or for which blocks have been received. + maxNumRequests = 64 +) + +type consensusReactor interface { + // for when we switch from blockchain reactor and fast sync to + // the consensus machine + SwitchToConsensus(sm.State, int) +} + +// BlockchainReactor handles long-term catchup syncing. +type BlockchainReactor struct { + p2p.BaseReactor + + initialState sm.State // immutable + state sm.State + + blockExec *sm.BlockExecutor + store *store.BlockStore + + fastSync bool + + fsm *BcReactorFSM + blocksSynced int + + // Receive goroutine forwards messages to this channel to be processed in the context of the poolRoutine. + messagesForFSMCh chan bcReactorMessage + + // Switch goroutine may send RemovePeer to the blockchain reactor. This is an error message that is relayed + // to this channel to be processed in the context of the poolRoutine. + errorsForFSMCh chan bcReactorMessage + + // This channel is used by the FSM and indirectly the block pool to report errors to the blockchain reactor and + // the switch. + eventsFromFSMCh chan bcFsmMessage + + swReporter *behaviour.SwitchReporter +} + +// NewBlockchainReactor returns new reactor instance. +func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *store.BlockStore, + fastSync bool) *BlockchainReactor { + + if state.LastBlockHeight != store.Height() { + panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, + store.Height())) + } + + const capacity = 1000 + eventsFromFSMCh := make(chan bcFsmMessage, capacity) + messagesForFSMCh := make(chan bcReactorMessage, capacity) + errorsForFSMCh := make(chan bcReactorMessage, capacity) + + startHeight := store.Height() + 1 + bcR := &BlockchainReactor{ + initialState: state, + state: state, + blockExec: blockExec, + fastSync: fastSync, + store: store, + messagesForFSMCh: messagesForFSMCh, + eventsFromFSMCh: eventsFromFSMCh, + errorsForFSMCh: errorsForFSMCh, + } + fsm := NewFSM(startHeight, bcR) + bcR.fsm = fsm + bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) + //bcR.swReporter = behaviour.NewSwitcReporter(bcR.BaseReactor.Switch) + + return bcR +} + +// bcReactorMessage is used by the reactor to send messages to the FSM. +type bcReactorMessage struct { + event bReactorEvent + data bReactorEventData +} + +type bFsmEvent uint + +const ( + // message type events + peerErrorEv = iota + 1 + syncFinishedEv +) + +type bFsmEventData struct { + peerID p2p.ID + err error +} + +// bcFsmMessage is used by the FSM to send messages to the reactor +type bcFsmMessage struct { + event bFsmEvent + data bFsmEventData +} + +// SetLogger implements cmn.Service by setting the logger on reactor and pool. +func (bcR *BlockchainReactor) SetLogger(l log.Logger) { + bcR.BaseService.Logger = l + bcR.fsm.SetLogger(l) +} + +// OnStart implements cmn.Service. +func (bcR *BlockchainReactor) OnStart() error { + bcR.swReporter = behaviour.NewSwitcReporter(bcR.BaseReactor.Switch) + if bcR.fastSync { + go bcR.poolRoutine() + } + return nil +} + +// OnStop implements cmn.Service. +func (bcR *BlockchainReactor) OnStop() { + _ = bcR.Stop() +} + +// GetChannels implements Reactor +func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { + return []*p2p.ChannelDescriptor{ + { + ID: BlockchainChannel, + Priority: 10, + SendQueueCapacity: 2000, + RecvBufferCapacity: 50 * 4096, + RecvMessageCapacity: maxMsgSize, + }, + } +} + +// AddPeer implements Reactor by sending our state to peer. +func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) + if !peer.Send(BlockchainChannel, msgBytes) { + // doing nothing, will try later in `poolRoutine` + } + // peer is added to the pool once we receive the first + // bcStatusResponseMessage from the peer and call pool.updatePeer() +} + +// sendBlockToPeer loads a block and sends it to the requesting peer. +// If the block doesn't exist a bcNoBlockResponseMessage is sent. +// If all nodes are honest, no node should be requesting for a block that doesn't exist. +func (bcR *BlockchainReactor) sendBlockToPeer(msg *bcBlockRequestMessage, + src p2p.Peer) (queued bool) { + + block := bcR.store.LoadBlock(msg.Height) + if block != nil { + msgBytes := cdc.MustMarshalBinaryBare(&bcBlockResponseMessage{Block: block}) + return src.TrySend(BlockchainChannel, msgBytes) + } + + bcR.Logger.Info("peer asking for a block we don't have", "src", src, "height", msg.Height) + + msgBytes := cdc.MustMarshalBinaryBare(&bcNoBlockResponseMessage{Height: msg.Height}) + return src.TrySend(BlockchainChannel, msgBytes) +} + +func (bcR *BlockchainReactor) sendStatusResponseToPeer(msg *bcStatusRequestMessage, src p2p.Peer) (queued bool) { + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) + return src.TrySend(BlockchainChannel, msgBytes) +} + +// RemovePeer implements Reactor by removing peer from the pool. +func (bcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { + msgData := bcReactorMessage{ + event: peerRemoveEv, + data: bReactorEventData{ + peerID: peer.ID(), + err: errSwitchRemovesPeer, + }, + } + bcR.errorsForFSMCh <- msgData +} + +// Receive implements Reactor by handling 4 types of messages (look below). +func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { + msg, err := decodeMsg(msgBytes) + if err != nil { + bcR.Logger.Error("error decoding message", + "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + _ = bcR.swReporter.Report(behaviour.BadMessage(src.ID(), err.Error())) + return + } + + if err = msg.ValidateBasic(); err != nil { + bcR.Logger.Error("peer sent us invalid msg", "peer", src, "msg", msg, "err", err) + _ = bcR.swReporter.Report(behaviour.BadMessage(src.ID(), err.Error())) + return + } + + bcR.Logger.Debug("Receive", "src", src, "chID", chID, "msg", msg) + + switch msg := msg.(type) { + case *bcBlockRequestMessage: + if queued := bcR.sendBlockToPeer(msg, src); !queued { + // Unfortunately not queued since the queue is full. + bcR.Logger.Error("Could not send block message to peer", "src", src, "height", msg.Height) + } + + case *bcStatusRequestMessage: + // Send peer our state. + if queued := bcR.sendStatusResponseToPeer(msg, src); !queued { + // Unfortunately not queued since the queue is full. + bcR.Logger.Error("Could not send status message to peer", "src", src) + } + + case *bcBlockResponseMessage: + msgForFSM := bcReactorMessage{ + event: blockResponseEv, + data: bReactorEventData{ + peerID: src.ID(), + height: msg.Block.Height, + block: msg.Block, + length: len(msgBytes), + }, + } + bcR.Logger.Info("Received", "src", src, "height", msg.Block.Height) + bcR.messagesForFSMCh <- msgForFSM + + case *bcStatusResponseMessage: + // Got a peer status. Unverified. + msgForFSM := bcReactorMessage{ + event: statusResponseEv, + data: bReactorEventData{ + peerID: src.ID(), + height: msg.Height, + length: len(msgBytes), + }, + } + bcR.messagesForFSMCh <- msgForFSM + + default: + bcR.Logger.Error(fmt.Sprintf("unknown message type %v", reflect.TypeOf(msg))) + } +} + +// processBlocksRoutine processes blocks until signlaed to stop over the stopProcessing channel +func (bcR *BlockchainReactor) processBlocksRoutine(stopProcessing chan struct{}) { + + processReceivedBlockTicker := time.NewTicker(trySyncIntervalMS * time.Millisecond) + doProcessBlockCh := make(chan struct{}, 1) + + lastHundred := time.Now() + lastRate := 0.0 + +ForLoop: + for { + select { + case <-stopProcessing: + bcR.Logger.Info("finishing block execution") + break ForLoop + case <-processReceivedBlockTicker.C: // try to execute blocks + select { + case doProcessBlockCh <- struct{}{}: + default: + } + case <-doProcessBlockCh: + for { + err := bcR.processBlock() + if err == errMissingBlock { + break + } + // Notify FSM of block processing result. + msgForFSM := bcReactorMessage{ + event: processedBlockEv, + data: bReactorEventData{ + err: err, + }, + } + _ = bcR.fsm.Handle(&msgForFSM) + + if err != nil { + break + } + + bcR.blocksSynced++ + if bcR.blocksSynced%100 == 0 { + lastRate = 0.9*lastRate + 0.1*(100/time.Since(lastHundred).Seconds()) + height, maxPeerHeight := bcR.fsm.Status() + bcR.Logger.Info("Fast Sync Rate", "height", height, + "max_peer_height", maxPeerHeight, "blocks/s", lastRate) + lastHundred = time.Now() + } + } + } + } +} + +// poolRoutine receives and handles messages from the Receive() routine and from the FSM. +func (bcR *BlockchainReactor) poolRoutine() { + + bcR.fsm.Start() + + sendBlockRequestTicker := time.NewTicker(trySendIntervalMS * time.Millisecond) + statusUpdateTicker := time.NewTicker(statusUpdateIntervalSeconds * time.Second) + + stopProcessing := make(chan struct{}, 1) + go bcR.processBlocksRoutine(stopProcessing) + +ForLoop: + for { + select { + + case <-sendBlockRequestTicker.C: + if !bcR.fsm.NeedsBlocks() { + continue + } + _ = bcR.fsm.Handle(&bcReactorMessage{ + event: makeRequestsEv, + data: bReactorEventData{ + maxNumRequests: maxNumRequests}}) + + case <-statusUpdateTicker.C: + // Ask for status updates. + go bcR.sendStatusRequest() + + case msg := <-bcR.messagesForFSMCh: + // Sent from the Receive() routine when status (statusResponseEv) and + // block (blockResponseEv) response events are received + _ = bcR.fsm.Handle(&msg) + + case msg := <-bcR.errorsForFSMCh: + // Sent from the switch.RemovePeer() routine (RemovePeerEv) and + // FSM state timer expiry routine (stateTimeoutEv). + _ = bcR.fsm.Handle(&msg) + + case msg := <-bcR.eventsFromFSMCh: + switch msg.event { + case syncFinishedEv: + stopProcessing <- struct{}{} + // Sent from the FSM when it enters finished state. + break ForLoop + case peerErrorEv: + // Sent from the FSM when it detects peer error + bcR.reportPeerErrorToSwitch(msg.data.err, msg.data.peerID) + if msg.data.err == errNoPeerResponse { + // Sent from the peer timeout handler routine + _ = bcR.fsm.Handle(&bcReactorMessage{ + event: peerRemoveEv, + data: bReactorEventData{ + peerID: msg.data.peerID, + err: msg.data.err, + }, + }) + } else { + // For slow peers, or errors due to blocks received from wrong peer + // the FSM had already removed the peers + } + default: + bcR.Logger.Error("Event from FSM not supported", "type", msg.event) + } + + case <-bcR.Quit(): + break ForLoop + } + } +} + +func (bcR *BlockchainReactor) reportPeerErrorToSwitch(err error, peerID p2p.ID) { + peer := bcR.Switch.Peers().Get(peerID) + if peer != nil { + _ = bcR.swReporter.Report(behaviour.BadMessage(peerID, err.Error())) + } +} + +func (bcR *BlockchainReactor) processBlock() error { + + first, second, err := bcR.fsm.FirstTwoBlocks() + if err != nil { + // We need both to sync the first block. + return err + } + + chainID := bcR.initialState.ChainID + + firstParts := first.MakePartSet(types.BlockPartSizeBytes) + firstPartsHeader := firstParts.Header() + firstID := types.BlockID{Hash: first.Hash(), PartsHeader: firstPartsHeader} + // Finally, verify the first block using the second's commit + // NOTE: we can probably make this more efficient, but note that calling + // first.Hash() doesn't verify the tx contents, so MakePartSet() is + // currently necessary. + err = bcR.state.Validators.VerifyCommit(chainID, firstID, first.Height, second.LastCommit) + if err != nil { + bcR.Logger.Error("error during commit verification", "err", err, + "first", first.Height, "second", second.Height) + return errBlockVerificationFailure + } + + bcR.store.SaveBlock(first, firstParts, second.LastCommit) + + bcR.state, err = bcR.blockExec.ApplyBlock(bcR.state, firstID, first) + if err != nil { + panic(fmt.Sprintf("failed to process committed block (%d:%X): %v", first.Height, first.Hash(), err)) + } + + return nil +} + +// Implements bcRNotifier +// sendStatusRequest broadcasts `BlockStore` height. +func (bcR *BlockchainReactor) sendStatusRequest() { + msgBytes := cdc.MustMarshalBinaryBare(&bcStatusRequestMessage{bcR.store.Height()}) + bcR.Switch.Broadcast(BlockchainChannel, msgBytes) +} + +// Implements bcRNotifier +// BlockRequest sends `BlockRequest` height. +func (bcR *BlockchainReactor) sendBlockRequest(peerID p2p.ID, height int64) error { + peer := bcR.Switch.Peers().Get(peerID) + if peer == nil { + return errNilPeerForBlockRequest + } + + msgBytes := cdc.MustMarshalBinaryBare(&bcBlockRequestMessage{height}) + queued := peer.TrySend(BlockchainChannel, msgBytes) + if !queued { + return errSendQueueFull + } + return nil +} + +// Implements bcRNotifier +func (bcR *BlockchainReactor) switchToConsensus() { + conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) + if ok { + conR.SwitchToConsensus(bcR.state, bcR.blocksSynced) + bcR.eventsFromFSMCh <- bcFsmMessage{event: syncFinishedEv} + } else { + // Should only happen during testing. + } +} + +// Implements bcRNotifier +// Called by FSM and pool: +// - pool calls when it detects slow peer or when peer times out +// - FSM calls when: +// - adding a block (addBlock) fails +// - reactor processing of a block reports failure and FSM sends back the peers of first and second blocks +func (bcR *BlockchainReactor) sendPeerError(err error, peerID p2p.ID) { + bcR.Logger.Info("sendPeerError:", "peer", peerID, "error", err) + msgData := bcFsmMessage{ + event: peerErrorEv, + data: bFsmEventData{ + peerID: peerID, + err: err, + }, + } + bcR.eventsFromFSMCh <- msgData +} + +// Implements bcRNotifier +func (bcR *BlockchainReactor) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { + if timer == nil { + panic("nil timer pointer parameter") + } + if *timer == nil { + *timer = time.AfterFunc(timeout, func() { + msg := bcReactorMessage{ + event: stateTimeoutEv, + data: bReactorEventData{ + stateName: name, + }, + } + bcR.errorsForFSMCh <- msg + }) + } else { + (*timer).Reset(timeout) + } +} + +//----------------------------------------------------------------------------- +// Messages + +// BlockchainMessage is a generic message for this reactor. +type BlockchainMessage interface { + ValidateBasic() error +} + +// RegisterBlockchainMessages registers the fast sync messages for amino encoding. +func RegisterBlockchainMessages(cdc *amino.Codec) { + cdc.RegisterInterface((*BlockchainMessage)(nil), nil) + cdc.RegisterConcrete(&bcBlockRequestMessage{}, "tendermint/blockchain/BlockRequest", nil) + cdc.RegisterConcrete(&bcBlockResponseMessage{}, "tendermint/blockchain/BlockResponse", nil) + cdc.RegisterConcrete(&bcNoBlockResponseMessage{}, "tendermint/blockchain/NoBlockResponse", nil) + cdc.RegisterConcrete(&bcStatusResponseMessage{}, "tendermint/blockchain/StatusResponse", nil) + cdc.RegisterConcrete(&bcStatusRequestMessage{}, "tendermint/blockchain/StatusRequest", nil) +} + +func decodeMsg(bz []byte) (msg BlockchainMessage, err error) { + if len(bz) > maxMsgSize { + return msg, fmt.Errorf("msg exceeds max size (%d > %d)", len(bz), maxMsgSize) + } + err = cdc.UnmarshalBinaryBare(bz, &msg) + return +} + +//------------------------------------- + +type bcBlockRequestMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcBlockRequestMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") + } + return nil +} + +func (m *bcBlockRequestMessage) String() string { + return fmt.Sprintf("[bcBlockRequestMessage %v]", m.Height) +} + +type bcNoBlockResponseMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcNoBlockResponseMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") + } + return nil +} + +func (m *bcNoBlockResponseMessage) String() string { + return fmt.Sprintf("[bcNoBlockResponseMessage %d]", m.Height) +} + +//------------------------------------- + +type bcBlockResponseMessage struct { + Block *types.Block +} + +// ValidateBasic performs basic validation. +func (m *bcBlockResponseMessage) ValidateBasic() error { + return m.Block.ValidateBasic() +} + +func (m *bcBlockResponseMessage) String() string { + return fmt.Sprintf("[bcBlockResponseMessage %v]", m.Block.Height) +} + +//------------------------------------- + +type bcStatusRequestMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcStatusRequestMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") + } + return nil +} + +func (m *bcStatusRequestMessage) String() string { + return fmt.Sprintf("[bcStatusRequestMessage %v]", m.Height) +} + +//------------------------------------- + +type bcStatusResponseMessage struct { + Height int64 +} + +// ValidateBasic performs basic validation. +func (m *bcStatusResponseMessage) ValidateBasic() error { + if m.Height < 0 { + return errors.New("negative Height") + } + return nil +} + +func (m *bcStatusResponseMessage) String() string { + return fmt.Sprintf("[bcStatusResponseMessage %v]", m.Height) +} diff --git a/blockchain/v1/reactor_fsm.go b/blockchain/v1/reactor_fsm.go new file mode 100644 index 000000000..4bfef64ea --- /dev/null +++ b/blockchain/v1/reactor_fsm.go @@ -0,0 +1,450 @@ +package v1 + +import ( + "errors" + "fmt" + "sync" + "time" + + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +// Blockchain Reactor State +type bcReactorFSMState struct { + name string + + // called when transitioning out of current state + handle func(*BcReactorFSM, bReactorEvent, bReactorEventData) (next *bcReactorFSMState, err error) + // called when entering the state + enter func(fsm *BcReactorFSM) + + // timeout to ensure FSM is not stuck in a state forever + // the timer is owned and run by the fsm instance + timeout time.Duration +} + +func (s *bcReactorFSMState) String() string { + return s.name +} + +// BcReactorFSM is the datastructure for the Blockchain Reactor State Machine +type BcReactorFSM struct { + logger log.Logger + mtx sync.Mutex + + startTime time.Time + + state *bcReactorFSMState + stateTimer *time.Timer + pool *BlockPool + + // interface used to call the Blockchain reactor to send StatusRequest, BlockRequest, reporting errors, etc. + toBcR bcReactor +} + +// NewFSM creates a new reactor FSM. +func NewFSM(height int64, toBcR bcReactor) *BcReactorFSM { + return &BcReactorFSM{ + state: unknown, + startTime: time.Now(), + pool: NewBlockPool(height, toBcR), + toBcR: toBcR, + } +} + +// bReactorEventData is part of the message sent by the reactor to the FSM and used by the state handlers. +type bReactorEventData struct { + peerID p2p.ID + err error // for peer error: timeout, slow; for processed block event if error occurred + height int64 // for status response; for processed block event + block *types.Block // for block response + stateName string // for state timeout events + length int // for block response event, length of received block, used to detect slow peers + maxNumRequests int // for request needed event, maximum number of pending requests +} + +// Blockchain Reactor Events (the input to the state machine) +type bReactorEvent uint + +const ( + // message type events + startFSMEv = iota + 1 + statusResponseEv + blockResponseEv + processedBlockEv + makeRequestsEv + stopFSMEv + + // other events + peerRemoveEv = iota + 256 + stateTimeoutEv +) + +func (msg *bcReactorMessage) String() string { + var dataStr string + + switch msg.event { + case startFSMEv: + dataStr = "" + case statusResponseEv: + dataStr = fmt.Sprintf("peer=%v height=%v", msg.data.peerID, msg.data.height) + case blockResponseEv: + dataStr = fmt.Sprintf("peer=%v block.height=%v length=%v", + msg.data.peerID, msg.data.block.Height, msg.data.length) + case processedBlockEv: + dataStr = fmt.Sprintf("error=%v", msg.data.err) + case makeRequestsEv: + dataStr = "" + case stopFSMEv: + dataStr = "" + case peerRemoveEv: + dataStr = fmt.Sprintf("peer: %v is being removed by the switch", msg.data.peerID) + case stateTimeoutEv: + dataStr = fmt.Sprintf("state=%v", msg.data.stateName) + default: + dataStr = fmt.Sprintf("cannot interpret message data") + } + + return fmt.Sprintf("%v: %v", msg.event, dataStr) +} + +func (ev bReactorEvent) String() string { + switch ev { + case startFSMEv: + return "startFSMEv" + case statusResponseEv: + return "statusResponseEv" + case blockResponseEv: + return "blockResponseEv" + case processedBlockEv: + return "processedBlockEv" + case makeRequestsEv: + return "makeRequestsEv" + case stopFSMEv: + return "stopFSMEv" + case peerRemoveEv: + return "peerRemoveEv" + case stateTimeoutEv: + return "stateTimeoutEv" + default: + return "event unknown" + } + +} + +// states +var ( + unknown *bcReactorFSMState + waitForPeer *bcReactorFSMState + waitForBlock *bcReactorFSMState + finished *bcReactorFSMState +) + +// timeouts for state timers +const ( + waitForPeerTimeout = 3 * time.Second + waitForBlockAtCurrentHeightTimeout = 10 * time.Second +) + +// errors +var ( + // internal to the package + errNoErrorFinished = errors.New("fast sync is finished") + errInvalidEvent = errors.New("invalid event in current state") + errMissingBlock = errors.New("missing blocks") + errNilPeerForBlockRequest = errors.New("peer for block request does not exist in the switch") + errSendQueueFull = errors.New("block request not made, send-queue is full") + errPeerTooShort = errors.New("peer height too low, old peer removed/ new peer not added") + errSwitchRemovesPeer = errors.New("switch is removing peer") + errTimeoutEventWrongState = errors.New("timeout event for a state different than the current one") + errNoTallerPeer = errors.New("fast sync timed out on waiting for a peer taller than this node") + + // reported eventually to the switch + errPeerLowersItsHeight = errors.New("fast sync peer reports a height lower than previous") // handle return + errNoPeerResponseForCurrentHeights = errors.New("fast sync timed out on peer block response for current heights") // handle return + errNoPeerResponse = errors.New("fast sync timed out on peer block response") // xx + errBadDataFromPeer = errors.New("fast sync received block from wrong peer or block is bad") // xx + errDuplicateBlock = errors.New("fast sync received duplicate block from peer") + errBlockVerificationFailure = errors.New("fast sync block verification failure") // xx + errSlowPeer = errors.New("fast sync peer is not sending us data fast enough") // xx + +) + +func init() { + unknown = &bcReactorFSMState{ + name: "unknown", + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + switch ev { + case startFSMEv: + // Broadcast Status message. Currently doesn't return non-nil error. + fsm.toBcR.sendStatusRequest() + return waitForPeer, nil + + case stopFSMEv: + return finished, errNoErrorFinished + + default: + return unknown, errInvalidEvent + } + }, + } + + waitForPeer = &bcReactorFSMState{ + name: "waitForPeer", + timeout: waitForPeerTimeout, + enter: func(fsm *BcReactorFSM) { + // Stop when leaving the state. + fsm.resetStateTimer() + }, + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + switch ev { + case stateTimeoutEv: + if data.stateName != "waitForPeer" { + fsm.logger.Error("received a state timeout event for different state", + "state", data.stateName) + return waitForPeer, errTimeoutEventWrongState + } + // There was no statusResponse received from any peer. + // Should we send status request again? + return finished, errNoTallerPeer + + case statusResponseEv: + if err := fsm.pool.UpdatePeer(data.peerID, data.height); err != nil { + if fsm.pool.NumPeers() == 0 { + return waitForPeer, err + } + } + if fsm.stateTimer != nil { + fsm.stateTimer.Stop() + } + return waitForBlock, nil + + case stopFSMEv: + if fsm.stateTimer != nil { + fsm.stateTimer.Stop() + } + return finished, errNoErrorFinished + + default: + return waitForPeer, errInvalidEvent + } + }, + } + + waitForBlock = &bcReactorFSMState{ + name: "waitForBlock", + timeout: waitForBlockAtCurrentHeightTimeout, + enter: func(fsm *BcReactorFSM) { + // Stop when leaving the state. + fsm.resetStateTimer() + }, + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + switch ev { + + case statusResponseEv: + err := fsm.pool.UpdatePeer(data.peerID, data.height) + if fsm.pool.NumPeers() == 0 { + return waitForPeer, err + } + if fsm.pool.ReachedMaxHeight() { + return finished, err + } + return waitForBlock, err + + case blockResponseEv: + fsm.logger.Debug("blockResponseEv", "H", data.block.Height) + err := fsm.pool.AddBlock(data.peerID, data.block, data.length) + if err != nil { + // A block was received that was unsolicited, from unexpected peer, or that we already have it. + // Ignore block, remove peer and send error to switch. + fsm.pool.RemovePeer(data.peerID, err) + fsm.toBcR.sendPeerError(err, data.peerID) + } + if fsm.pool.NumPeers() == 0 { + return waitForPeer, err + } + return waitForBlock, err + + case processedBlockEv: + if data.err != nil { + first, second, _ := fsm.pool.FirstTwoBlocksAndPeers() + fsm.logger.Error("error processing block", "err", data.err, + "first", first.block.Height, "second", second.block.Height) + fsm.logger.Error("send peer error for", "peer", first.peer.ID) + fsm.toBcR.sendPeerError(data.err, first.peer.ID) + fsm.logger.Error("send peer error for", "peer", second.peer.ID) + fsm.toBcR.sendPeerError(data.err, second.peer.ID) + // Remove the first two blocks. This will also remove the peers + fsm.pool.InvalidateFirstTwoBlocks(data.err) + } else { + fsm.pool.ProcessedCurrentHeightBlock() + // Since we advanced one block reset the state timer + fsm.resetStateTimer() + } + + // Both cases above may result in achieving maximum height. + if fsm.pool.ReachedMaxHeight() { + return finished, nil + } + + return waitForBlock, data.err + + case peerRemoveEv: + // This event is sent by the switch to remove disconnected and errored peers. + fsm.pool.RemovePeer(data.peerID, data.err) + if fsm.pool.NumPeers() == 0 { + return waitForPeer, nil + } + if fsm.pool.ReachedMaxHeight() { + return finished, nil + } + return waitForBlock, nil + + case makeRequestsEv: + fsm.makeNextRequests(data.maxNumRequests) + return waitForBlock, nil + + case stateTimeoutEv: + if data.stateName != "waitForBlock" { + fsm.logger.Error("received a state timeout event for different state", + "state", data.stateName) + return waitForBlock, errTimeoutEventWrongState + } + // We haven't received the block at current height or height+1. Remove peer. + fsm.pool.RemovePeerAtCurrentHeights(errNoPeerResponseForCurrentHeights) + fsm.resetStateTimer() + if fsm.pool.NumPeers() == 0 { + return waitForPeer, errNoPeerResponseForCurrentHeights + } + if fsm.pool.ReachedMaxHeight() { + return finished, nil + } + return waitForBlock, errNoPeerResponseForCurrentHeights + + case stopFSMEv: + if fsm.stateTimer != nil { + fsm.stateTimer.Stop() + } + return finished, errNoErrorFinished + + default: + return waitForBlock, errInvalidEvent + } + }, + } + + finished = &bcReactorFSMState{ + name: "finished", + enter: func(fsm *BcReactorFSM) { + fsm.logger.Info("Time to switch to consensus reactor!", "height", fsm.pool.Height) + fsm.toBcR.switchToConsensus() + fsm.cleanup() + }, + handle: func(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) (*bcReactorFSMState, error) { + return finished, nil + }, + } +} + +// Interface used by FSM for sending Block and Status requests, +// informing of peer errors and state timeouts +// Implemented by BlockchainReactor and tests +type bcReactor interface { + sendStatusRequest() + sendBlockRequest(peerID p2p.ID, height int64) error + sendPeerError(err error, peerID p2p.ID) + resetStateTimer(name string, timer **time.Timer, timeout time.Duration) + switchToConsensus() +} + +// SetLogger sets the FSM logger. +func (fsm *BcReactorFSM) SetLogger(l log.Logger) { + fsm.logger = l + fsm.pool.SetLogger(l) +} + +// Start starts the FSM. +func (fsm *BcReactorFSM) Start() { + _ = fsm.Handle(&bcReactorMessage{event: startFSMEv}) +} + +// Handle processes messages and events sent to the FSM. +func (fsm *BcReactorFSM) Handle(msg *bcReactorMessage) error { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + fsm.logger.Debug("FSM received", "event", msg, "state", fsm.state) + + if fsm.state == nil { + fsm.state = unknown + } + next, err := fsm.state.handle(fsm, msg.event, msg.data) + if err != nil { + fsm.logger.Error("FSM event handler returned", "err", err, + "state", fsm.state, "event", msg.event) + } + + oldState := fsm.state.name + fsm.transition(next) + if oldState != fsm.state.name { + fsm.logger.Info("FSM changed state", "new_state", fsm.state) + } + return err +} + +func (fsm *BcReactorFSM) transition(next *bcReactorFSMState) { + if next == nil { + return + } + if fsm.state != next { + fsm.state = next + if next.enter != nil { + next.enter(fsm) + } + } +} + +// Called when entering an FSM state in order to detect lack of progress in the state machine. +// Note the use of the 'bcr' interface to facilitate testing without timer expiring. +func (fsm *BcReactorFSM) resetStateTimer() { + fsm.toBcR.resetStateTimer(fsm.state.name, &fsm.stateTimer, fsm.state.timeout) +} + +func (fsm *BcReactorFSM) isCaughtUp() bool { + return fsm.state == finished +} + +func (fsm *BcReactorFSM) makeNextRequests(maxNumRequests int) { + fsm.pool.MakeNextRequests(maxNumRequests) +} + +func (fsm *BcReactorFSM) cleanup() { + fsm.pool.Cleanup() +} + +// NeedsBlocks checks if more block requests are required. +func (fsm *BcReactorFSM) NeedsBlocks() bool { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + return fsm.state.name == "waitForBlock" && fsm.pool.NeedsBlocks() +} + +// FirstTwoBlocks returns the two blocks at pool height and height+1 +func (fsm *BcReactorFSM) FirstTwoBlocks() (first, second *types.Block, err error) { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + firstBP, secondBP, err := fsm.pool.FirstTwoBlocksAndPeers() + if err == nil { + first = firstBP.block + second = secondBP.block + } + return +} + +// Status returns the pool's height and the maximum peer height. +func (fsm *BcReactorFSM) Status() (height, maxPeerHeight int64) { + fsm.mtx.Lock() + defer fsm.mtx.Unlock() + return fsm.pool.Height, fsm.pool.MaxPeerHeight +} diff --git a/blockchain/v1/reactor_fsm_test.go b/blockchain/v1/reactor_fsm_test.go new file mode 100644 index 000000000..54e177f25 --- /dev/null +++ b/blockchain/v1/reactor_fsm_test.go @@ -0,0 +1,938 @@ +package v1 + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/types" +) + +type lastBlockRequestT struct { + peerID p2p.ID + height int64 +} + +type lastPeerErrorT struct { + peerID p2p.ID + err error +} + +// reactor for FSM testing +type testReactor struct { + logger log.Logger + fsm *BcReactorFSM + numStatusRequests int + numBlockRequests int + lastBlockRequest lastBlockRequestT + lastPeerError lastPeerErrorT + stateTimerStarts map[string]int +} + +func sendEventToFSM(fsm *BcReactorFSM, ev bReactorEvent, data bReactorEventData) error { + return fsm.Handle(&bcReactorMessage{event: ev, data: data}) +} + +type fsmStepTestValues struct { + currentState string + event bReactorEvent + data bReactorEventData + + wantErr error + wantState string + wantStatusReqSent bool + wantReqIncreased bool + wantNewBlocks []int64 + wantRemovedPeers []p2p.ID +} + +// --------------------------------------------------------------------------- +// helper test function for different FSM events, state and expected behavior +func sStopFSMEv(current, expected string) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: stopFSMEv, + wantState: expected, + wantErr: errNoErrorFinished} +} + +func sUnknownFSMEv(current string) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: 1234, + wantState: current, + wantErr: errInvalidEvent} +} + +func sStartFSMEv() fsmStepTestValues { + return fsmStepTestValues{ + currentState: "unknown", + event: startFSMEv, + wantState: "waitForPeer", + wantStatusReqSent: true} +} + +func sStateTimeoutEv(current, expected string, timedoutState string, wantErr error) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: stateTimeoutEv, + data: bReactorEventData{ + stateName: timedoutState, + }, + wantState: expected, + wantErr: wantErr, + } +} + +func sProcessedBlockEv(current, expected string, reactorError error) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: processedBlockEv, + data: bReactorEventData{ + err: reactorError, + }, + wantState: expected, + wantErr: reactorError, + } +} + +func sStatusEv(current, expected string, peerID p2p.ID, height int64, err error) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: statusResponseEv, + data: bReactorEventData{peerID: peerID, height: height}, + wantState: expected, + wantErr: err} +} + +func sMakeRequestsEv(current, expected string, maxPendingRequests int) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: makeRequestsEv, + data: bReactorEventData{maxNumRequests: maxPendingRequests}, + wantState: expected, + wantReqIncreased: true, + } +} + +func sMakeRequestsEvErrored(current, expected string, + maxPendingRequests int, err error, peersRemoved []p2p.ID) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: makeRequestsEv, + data: bReactorEventData{maxNumRequests: maxPendingRequests}, + wantState: expected, + wantErr: err, + wantRemovedPeers: peersRemoved, + wantReqIncreased: true, + } +} + +func sBlockRespEv(current, expected string, peerID p2p.ID, height int64, prevBlocks []int64) fsmStepTestValues { + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + return fsmStepTestValues{ + currentState: current, + event: blockResponseEv, + data: bReactorEventData{ + peerID: peerID, + height: height, + block: types.MakeBlock(int64(height), txs, nil, nil), + length: 100}, + wantState: expected, + wantNewBlocks: append(prevBlocks, height), + } +} + +func sBlockRespEvErrored(current, expected string, + peerID p2p.ID, height int64, prevBlocks []int64, wantErr error, peersRemoved []p2p.ID) fsmStepTestValues { + txs := []types.Tx{types.Tx("foo"), types.Tx("bar")} + + return fsmStepTestValues{ + currentState: current, + event: blockResponseEv, + data: bReactorEventData{ + peerID: peerID, + height: height, + block: types.MakeBlock(int64(height), txs, nil, nil), + length: 100}, + wantState: expected, + wantErr: wantErr, + wantRemovedPeers: peersRemoved, + wantNewBlocks: prevBlocks, + } +} + +func sPeerRemoveEv(current, expected string, peerID p2p.ID, err error, peersRemoved []p2p.ID) fsmStepTestValues { + return fsmStepTestValues{ + currentState: current, + event: peerRemoveEv, + data: bReactorEventData{ + peerID: peerID, + err: err, + }, + wantState: expected, + wantRemovedPeers: peersRemoved, + } +} + +// -------------------------------------------- + +func newTestReactor(height int64) *testReactor { + testBcR := &testReactor{logger: log.TestingLogger(), stateTimerStarts: make(map[string]int)} + testBcR.fsm = NewFSM(height, testBcR) + testBcR.fsm.SetLogger(testBcR.logger) + return testBcR +} + +func fixBlockResponseEvStep(step *fsmStepTestValues, testBcR *testReactor) { + // There is currently no good way to know to which peer a block request was sent. + // So in some cases where it does not matter, before we simulate a block response + // we cheat and look where it is expected from. + if step.event == blockResponseEv { + height := step.data.height + peerID, ok := testBcR.fsm.pool.blocks[height] + if ok { + step.data.peerID = peerID + } + } +} + +type testFields struct { + name string + startingHeight int64 + maxRequestsPerPeer int + maxPendingRequests int + steps []fsmStepTestValues +} + +func executeFSMTests(t *testing.T, tests []testFields, matchRespToReq bool) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test reactor + testBcR := newTestReactor(tt.startingHeight) + + if tt.maxRequestsPerPeer != 0 { + maxRequestsPerPeer = tt.maxRequestsPerPeer + } + + for _, step := range tt.steps { + assert.Equal(t, step.currentState, testBcR.fsm.state.name) + + var heightBefore int64 + if step.event == processedBlockEv && step.data.err == errBlockVerificationFailure { + heightBefore = testBcR.fsm.pool.Height + } + oldNumStatusRequests := testBcR.numStatusRequests + oldNumBlockRequests := testBcR.numBlockRequests + if matchRespToReq { + fixBlockResponseEvStep(&step, testBcR) + } + + fsmErr := sendEventToFSM(testBcR.fsm, step.event, step.data) + assert.Equal(t, step.wantErr, fsmErr) + + if step.wantStatusReqSent { + assert.Equal(t, oldNumStatusRequests+1, testBcR.numStatusRequests) + } else { + assert.Equal(t, oldNumStatusRequests, testBcR.numStatusRequests) + } + + if step.wantReqIncreased { + assert.True(t, oldNumBlockRequests < testBcR.numBlockRequests) + } else { + assert.Equal(t, oldNumBlockRequests, testBcR.numBlockRequests) + } + + for _, height := range step.wantNewBlocks { + _, err := testBcR.fsm.pool.BlockAndPeerAtHeight(height) + assert.Nil(t, err) + } + if step.event == processedBlockEv && step.data.err == errBlockVerificationFailure { + heightAfter := testBcR.fsm.pool.Height + assert.Equal(t, heightBefore, heightAfter) + firstAfter, err1 := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height) + secondAfter, err2 := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height + 1) + assert.NotNil(t, err1) + assert.NotNil(t, err2) + assert.Nil(t, firstAfter) + assert.Nil(t, secondAfter) + } + + assert.Equal(t, step.wantState, testBcR.fsm.state.name) + + if step.wantState == "finished" { + assert.True(t, testBcR.fsm.isCaughtUp()) + } + } + }) + } +} + +func TestFSMBasic(t *testing.T) { + tests := []testFields{ + { + name: "one block, one peer - TS2", + startingHeight: 1, + maxRequestsPerPeer: 2, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 2, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{1}), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + { + name: "multi block, multi peer - TS2", + startingHeight: 1, + maxRequestsPerPeer: 2, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 4, nil), + sStatusEv("waitForBlock", "waitForBlock", "P2", 4, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{1, 2}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 4, []int64{1, 2, 3}), + + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + } + + executeFSMTests(t, tests, true) +} + +func TestFSMBlockVerificationFailure(t *testing.T) { + tests := []testFields{ + { + name: "block verification failure - TS2 variant", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + + // add P1 and get blocks 1-3 from it + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 3, []int64{1, 2}), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + + // process block failure, should remove P1 and all blocks + sProcessedBlockEv("waitForBlock", "waitForBlock", errBlockVerificationFailure), + + // get blocks 1-3 from P2 + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{1, 2}), + + // finish after processing blocks 1 and 2 + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMBadBlockFromPeer(t *testing.T) { + tests := []testFields{ + { + name: "block we haven't asked for", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and ask for blocks 1-3 + sStatusEv("waitForPeer", "waitForBlock", "P1", 300, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // blockResponseEv for height 100 should cause an error + sBlockRespEvErrored("waitForBlock", "waitForPeer", + "P1", 100, []int64{}, errMissingBlock, []p2p.ID{}), + }, + }, + { + name: "block we already have", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and get block 1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 100, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", + "P1", 1, []int64{}), + + // Get block 1 again. Since peer is removed together with block 1, + // the blocks present in the pool should be {} + sBlockRespEvErrored("waitForBlock", "waitForPeer", + "P1", 1, []int64{}, errDuplicateBlock, []p2p.ID{"P1"}), + }, + }, + { + name: "block from unknown peer", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and get block 1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + + // get block 1 from unknown peer P2 + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEvErrored("waitForBlock", "waitForBlock", + "P2", 1, []int64{}, errBadDataFromPeer, []p2p.ID{"P2"}), + }, + }, + { + name: "block from wrong peer", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, make requests for blocks 1-3 to P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + + // receive block 1 from P2 + sBlockRespEvErrored("waitForBlock", "waitForBlock", + "P2", 1, []int64{}, errBadDataFromPeer, []p2p.ID{"P2"}), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMBlockAtCurrentHeightDoesNotArriveInTime(t *testing.T) { + tests := []testFields{ + { + name: "block at current height undelivered - TS5", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, get blocks 1 and 2, process block 1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", + "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", + "P1", 2, []int64{1}), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + + // timeout on block 3, P1 should be removed + sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForBlock", errNoPeerResponseForCurrentHeights), + + // make requests and finish by receiving blocks 2 and 3 from P2 + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 2, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P2", 3, []int64{2}), + sProcessedBlockEv("waitForBlock", "finished", nil), + }, + }, + { + name: "block at current height undelivered, at maxPeerHeight after peer removal - TS3", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, request blocks 1-3 from P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // add P2 (tallest) + sStatusEv("waitForBlock", "waitForBlock", "P2", 30, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // receive blocks 1-3 from P1 + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 1, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 2, []int64{1}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 3, []int64{1, 2}), + + // process blocks at heights 1 and 2 + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // timeout on block at height 4 + sStateTimeoutEv("waitForBlock", "finished", "waitForBlock", nil), + }, + }, + } + + executeFSMTests(t, tests, true) +} + +func TestFSMPeerRelatedEvents(t *testing.T) { + tests := []testFields{ + { + name: "peer remove event with no blocks", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1, P2, P3 + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + sStatusEv("waitForBlock", "waitForBlock", "P3", 3, nil), + + // switch removes P2 + sPeerRemoveEv("waitForBlock", "waitForBlock", "P2", errSwitchRemovesPeer, []p2p.ID{"P2"}), + }, + }, + { + name: "only peer removed while in waitForBlock state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), + + // switch removes P1 + sPeerRemoveEv("waitForBlock", "waitForPeer", "P1", errSwitchRemovesPeer, []p2p.ID{"P1"}), + }, + }, + { + name: "highest peer removed while in waitForBlock state, node reaches maxPeerHeight - TS4 ", + startingHeight: 100, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and make requests + sStatusEv("waitForPeer", "waitForBlock", "P1", 101, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 200, nil), + + // get blocks 100 and 101 from P1 and process block at height 100 + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 100, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 101, []int64{100}), + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // switch removes peer P1, should be finished + sPeerRemoveEv("waitForBlock", "finished", "P2", errSwitchRemovesPeer, []p2p.ID{"P2"}), + }, + }, + { + name: "highest peer lowers its height in waitForBlock state, node reaches maxPeerHeight - TS4", + startingHeight: 100, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 and make requests + sStatusEv("waitForPeer", "waitForBlock", "P1", 101, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + + // add P2 + sStatusEv("waitForBlock", "waitForBlock", "P2", 200, nil), + + // get blocks 100 and 101 from P1 + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 100, []int64{}), + sBlockRespEv("waitForBlock", "waitForBlock", "P1", 101, []int64{100}), + + // processed block at heights 100 + sProcessedBlockEv("waitForBlock", "waitForBlock", nil), + + // P2 becomes short + sStatusEv("waitForBlock", "finished", "P2", 100, errPeerLowersItsHeight), + }, + }, + { + name: "new short peer while in waitForPeer state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForPeer", "P1", 3, errPeerTooShort), + }, + }, + { + name: "new short peer while in waitForBlock state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, errPeerTooShort), + }, + }, + { + name: "only peer updated with low height while in waitForBlock state", + startingHeight: 100, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 200, nil), + sStatusEv("waitForBlock", "waitForPeer", "P1", 3, errPeerLowersItsHeight), + }, + }, + { + name: "peer does not exist in the switch", + startingHeight: 9999999, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + // add P1 + sStatusEv("waitForPeer", "waitForBlock", "P1", 20000000, nil), + // send request for block 9999999 + // Note: For this block request the "switch missing the peer" error is simulated, + // see implementation of bcReactor interface, sendBlockRequest(), in this file. + sMakeRequestsEvErrored("waitForBlock", "waitForBlock", + maxNumRequests, nil, []p2p.ID{"P1"}), + }, + }, + } + + executeFSMTests(t, tests, true) +} + +func TestFSMStopFSM(t *testing.T) { + tests := []testFields{ + { + name: "stopFSMEv in unknown", + steps: []fsmStepTestValues{ + sStopFSMEv("unknown", "finished"), + }, + }, + { + name: "stopFSMEv in waitForPeer", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStopFSMEv("waitForPeer", "finished"), + }, + }, + { + name: "stopFSMEv in waitForBlock", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sStopFSMEv("waitForBlock", "finished"), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMUnknownElements(t *testing.T) { + tests := []testFields{ + { + name: "unknown event for state unknown", + steps: []fsmStepTestValues{ + sUnknownFSMEv("unknown"), + }, + }, + { + name: "unknown event for state waitForPeer", + steps: []fsmStepTestValues{ + sStartFSMEv(), + sUnknownFSMEv("waitForPeer"), + }, + }, + { + name: "unknown event for state waitForBlock", + startingHeight: 1, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sUnknownFSMEv("waitForBlock"), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func TestFSMPeerStateTimeoutEvent(t *testing.T) { + tests := []testFields{ + { + name: "timeout event for state waitForPeer while in state waitForPeer - TS1", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStateTimeoutEv("waitForPeer", "finished", "waitForPeer", errNoTallerPeer), + }, + }, + { + name: "timeout event for state waitForPeer while in a state != waitForPeer", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStateTimeoutEv("waitForPeer", "waitForPeer", "waitForBlock", errTimeoutEventWrongState), + }, + }, + { + name: "timeout event for state waitForBlock while in state waitForBlock ", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sStateTimeoutEv("waitForBlock", "waitForPeer", "waitForBlock", errNoPeerResponseForCurrentHeights), + }, + }, + { + name: "timeout event for state waitForBlock while in a state != waitForBlock", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForPeer", errTimeoutEventWrongState), + }, + }, + { + name: "timeout event for state waitForBlock with multiple peers", + startingHeight: 1, + maxRequestsPerPeer: 3, + steps: []fsmStepTestValues{ + sStartFSMEv(), + sStatusEv("waitForPeer", "waitForBlock", "P1", 3, nil), + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + sStatusEv("waitForBlock", "waitForBlock", "P2", 3, nil), + sStateTimeoutEv("waitForBlock", "waitForBlock", "waitForBlock", errNoPeerResponseForCurrentHeights), + }, + }, + } + + executeFSMTests(t, tests, false) +} + +func makeCorrectTransitionSequence(startingHeight int64, numBlocks int64, numPeers int, randomPeerHeights bool, + maxRequestsPerPeer int, maxPendingRequests int) testFields { + + // Generate numPeers peers with random or numBlocks heights according to the randomPeerHeights flag. + peerHeights := make([]int64, numPeers) + for i := 0; i < numPeers; i++ { + if i == 0 { + peerHeights[0] = numBlocks + continue + } + if randomPeerHeights { + peerHeights[i] = int64(cmn.MaxInt(cmn.RandIntn(int(numBlocks)), int(startingHeight)+1)) + } else { + peerHeights[i] = numBlocks + } + } + + // Approximate the slice capacity to save time for appends. + testSteps := make([]fsmStepTestValues, 0, 3*numBlocks+int64(numPeers)) + + testName := fmt.Sprintf("%v-blocks %v-startingHeight %v-peers %v-maxRequestsPerPeer %v-maxNumRequests", + numBlocks, startingHeight, numPeers, maxRequestsPerPeer, maxPendingRequests) + + // Add startFSMEv step. + testSteps = append(testSteps, sStartFSMEv()) + + // For each peer, add statusResponseEv step. + for i := 0; i < numPeers; i++ { + peerName := fmt.Sprintf("P%d", i) + if i == 0 { + testSteps = append( + testSteps, + sStatusEv("waitForPeer", "waitForBlock", p2p.ID(peerName), peerHeights[i], nil)) + } else { + testSteps = append(testSteps, + sStatusEv("waitForBlock", "waitForBlock", p2p.ID(peerName), peerHeights[i], nil)) + } + } + + height := startingHeight + numBlocksReceived := 0 + prevBlocks := make([]int64, 0, maxPendingRequests) + +forLoop: + for i := 0; i < int(numBlocks); i++ { + + // Add the makeRequestEv step periodically. + if i%int(maxRequestsPerPeer) == 0 { + testSteps = append( + testSteps, + sMakeRequestsEv("waitForBlock", "waitForBlock", maxNumRequests), + ) + } + + // Add the blockRespEv step + testSteps = append( + testSteps, + sBlockRespEv("waitForBlock", "waitForBlock", + "P0", height, prevBlocks)) + prevBlocks = append(prevBlocks, height) + height++ + numBlocksReceived++ + + // Add the processedBlockEv step periodically. + if numBlocksReceived >= int(maxRequestsPerPeer) || height >= numBlocks { + for j := int(height) - numBlocksReceived; j < int(height); j++ { + if j >= int(numBlocks) { + // This is the last block that is processed, we should be in "finished" state. + testSteps = append( + testSteps, + sProcessedBlockEv("waitForBlock", "finished", nil)) + break forLoop + } + testSteps = append( + testSteps, + sProcessedBlockEv("waitForBlock", "waitForBlock", nil)) + } + numBlocksReceived = 0 + prevBlocks = make([]int64, 0, maxPendingRequests) + } + } + + return testFields{ + name: testName, + startingHeight: startingHeight, + maxRequestsPerPeer: maxRequestsPerPeer, + maxPendingRequests: maxPendingRequests, + steps: testSteps, + } +} + +const ( + maxStartingHeightTest = 100 + maxRequestsPerPeerTest = 20 + maxTotalPendingRequestsTest = 600 + maxNumPeersTest = 1000 + maxNumBlocksInChainTest = 10000 //should be smaller than 9999999 +) + +func makeCorrectTransitionSequenceWithRandomParameters() testFields { + // Generate a starting height for fast sync. + startingHeight := int64(cmn.RandIntn(maxStartingHeightTest) + 1) + + // Generate the number of requests per peer. + maxRequestsPerPeer := cmn.RandIntn(maxRequestsPerPeerTest) + 1 + + // Generate the maximum number of total pending requests, >= maxRequestsPerPeer. + maxPendingRequests := cmn.RandIntn(maxTotalPendingRequestsTest-int(maxRequestsPerPeer)) + maxRequestsPerPeer + + // Generate the number of blocks to be synced. + numBlocks := int64(cmn.RandIntn(maxNumBlocksInChainTest)) + startingHeight + + // Generate a number of peers. + numPeers := cmn.RandIntn(maxNumPeersTest) + 1 + + return makeCorrectTransitionSequence(startingHeight, numBlocks, numPeers, true, maxRequestsPerPeer, maxPendingRequests) +} + +func shouldApplyProcessedBlockEvStep(step *fsmStepTestValues, testBcR *testReactor) bool { + if step.event == processedBlockEv { + _, err := testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height) + if err == errMissingBlock { + return false + } + _, err = testBcR.fsm.pool.BlockAndPeerAtHeight(testBcR.fsm.pool.Height + 1) + if err == errMissingBlock { + return false + } + } + return true +} + +func TestFSMCorrectTransitionSequences(t *testing.T) { + + tests := []testFields{ + makeCorrectTransitionSequence(1, 100, 10, true, 10, 40), + makeCorrectTransitionSequenceWithRandomParameters(), + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create test reactor + testBcR := newTestReactor(tt.startingHeight) + + if tt.maxRequestsPerPeer != 0 { + maxRequestsPerPeer = tt.maxRequestsPerPeer + } + + for _, step := range tt.steps { + assert.Equal(t, step.currentState, testBcR.fsm.state.name) + + oldNumStatusRequests := testBcR.numStatusRequests + fixBlockResponseEvStep(&step, testBcR) + if !shouldApplyProcessedBlockEvStep(&step, testBcR) { + continue + } + + fsmErr := sendEventToFSM(testBcR.fsm, step.event, step.data) + assert.Equal(t, step.wantErr, fsmErr) + + if step.wantStatusReqSent { + assert.Equal(t, oldNumStatusRequests+1, testBcR.numStatusRequests) + } else { + assert.Equal(t, oldNumStatusRequests, testBcR.numStatusRequests) + } + + assert.Equal(t, step.wantState, testBcR.fsm.state.name) + if step.wantState == "finished" { + assert.True(t, testBcR.fsm.isCaughtUp()) + } + } + + }) + } +} + +// ---------------------------------------- +// implements the bcRNotifier +func (testR *testReactor) sendPeerError(err error, peerID p2p.ID) { + testR.logger.Info("Reactor received sendPeerError call from FSM", "peer", peerID, "err", err) + testR.lastPeerError.peerID = peerID + testR.lastPeerError.err = err +} + +func (testR *testReactor) sendStatusRequest() { + testR.logger.Info("Reactor received sendStatusRequest call from FSM") + testR.numStatusRequests++ +} + +func (testR *testReactor) sendBlockRequest(peerID p2p.ID, height int64) error { + testR.logger.Info("Reactor received sendBlockRequest call from FSM", "peer", peerID, "height", height) + testR.numBlockRequests++ + testR.lastBlockRequest.peerID = peerID + testR.lastBlockRequest.height = height + if height == 9999999 { + // simulate switch does not have peer + return errNilPeerForBlockRequest + } + return nil +} + +func (testR *testReactor) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { + testR.logger.Info("Reactor received resetStateTimer call from FSM", "state", name, "timeout", timeout) + if _, ok := testR.stateTimerStarts[name]; !ok { + testR.stateTimerStarts[name] = 1 + } else { + testR.stateTimerStarts[name]++ + } +} + +func (testR *testReactor) switchToConsensus() { +} + +// ---------------------------------------- diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go new file mode 100644 index 000000000..b5965a2af --- /dev/null +++ b/blockchain/v1/reactor_test.go @@ -0,0 +1,337 @@ +package v1 + +import ( + "fmt" + "os" + "sort" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + abci "github.com/tendermint/tendermint/abci/types" + cfg "github.com/tendermint/tendermint/config" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mock" + "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" + "github.com/tendermint/tendermint/types" + tmtime "github.com/tendermint/tendermint/types/time" + dbm "github.com/tendermint/tm-cmn/db" +) + +var config *cfg.Config + +func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.GenesisDoc, []types.PrivValidator) { + validators := make([]types.GenesisValidator, numValidators) + privValidators := make([]types.PrivValidator, numValidators) + for i := 0; i < numValidators; i++ { + val, privVal := types.RandValidator(randPower, minPower) + validators[i] = types.GenesisValidator{ + PubKey: val.PubKey, + Power: val.VotingPower, + } + privValidators[i] = privVal + } + sort.Sort(types.PrivValidatorsByAddress(privValidators)) + + return &types.GenesisDoc{ + GenesisTime: tmtime.Now(), + ChainID: config.ChainID(), + Validators: validators, + }, privValidators +} + +func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { + addr := privVal.GetPubKey().Address() + idx, _ := valset.GetByAddress(addr) + vote := &types.Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: header.Height, + Round: 1, + Timestamp: tmtime.Now(), + Type: types.PrecommitType, + BlockID: blockID, + } + + _ = privVal.SignVote(header.ChainID, vote) + + return vote +} + +type BlockchainReactorPair struct { + bcR *BlockchainReactor + conR *consensusReactorTest +} + +func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) *BlockchainReactor { + if len(privVals) != 1 { + panic("only support one validator") + } + + app := &testApp{} + cc := proxy.NewLocalClientCreator(app) + proxyApp := proxy.NewAppConns(cc) + err := proxyApp.Start() + if err != nil { + panic(cmn.ErrorWrap(err, "error start app")) + } + + blockDB := dbm.NewMemDB() + stateDB := dbm.NewMemDB() + blockStore := store.NewBlockStore(blockDB) + + state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) + if err != nil { + panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + } + + // Make the BlockchainReactor itself. + // NOTE we have to create and commit the blocks first because + // pool.height is determined from the store. + fastSync := true + db := dbm.NewMemDB() + blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), + mock.Mempool{}, sm.MockEvidencePool{}) + sm.SaveState(db, state) + + // let's add some blocks in + for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { + lastCommit := types.NewCommit(types.BlockID{}, nil) + if blockHeight > 1 { + lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) + lastBlock := blockStore.LoadBlock(blockHeight - 1) + + vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + } + + thisBlock := makeBlock(blockHeight, state, lastCommit) + + thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) + blockID := types.BlockID{Hash: thisBlock.Hash(), PartsHeader: thisParts.Header()} + + state, err = blockExec.ApplyBlock(state, blockID, thisBlock) + if err != nil { + panic(cmn.ErrorWrap(err, "error apply block")) + } + + blockStore.SaveBlock(thisBlock, thisParts, lastCommit) + } + + bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor.SetLogger(logger.With("module", "blockchain")) + + return bcReactor +} + +func newBlockchainReactorPair(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) BlockchainReactorPair { + + consensusReactor := &consensusReactorTest{} + consensusReactor.BaseReactor = *p2p.NewBaseReactor("Consensus reactor", consensusReactor) + + return BlockchainReactorPair{ + newBlockchainReactor(logger, genDoc, privVals, maxBlockHeight), + consensusReactor} +} + +type consensusReactorTest struct { + p2p.BaseReactor // BaseService + p2p.Switch + switchedToConsensus bool + mtx sync.Mutex +} + +func (conR *consensusReactorTest) SwitchToConsensus(state sm.State, blocksSynced int) { + conR.mtx.Lock() + defer conR.mtx.Unlock() + conR.switchedToConsensus = true +} + +func TestFastSyncNoBlockResponse(t *testing.T) { + + config = cfg.ResetTestRoot("blockchain_new_reactor_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + + maxBlockHeight := int64(65) + + reactorPairs := make([]BlockchainReactorPair, 2) + + logger := log.TestingLogger() + reactorPairs[0] = newBlockchainReactorPair(logger, genDoc, privVals, maxBlockHeight) + reactorPairs[1] = newBlockchainReactorPair(logger, genDoc, privVals, 0) + + p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { + s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) + s.AddReactor("CONSENSUS", reactorPairs[i].conR) + moduleName := fmt.Sprintf("blockchain-%v", i) + reactorPairs[i].bcR.SetLogger(logger.With("module", moduleName)) + + return s + + }, p2p.Connect2Switches) + + defer func() { + for _, r := range reactorPairs { + _ = r.bcR.Stop() + _ = r.conR.Stop() + } + }() + + tests := []struct { + height int64 + existent bool + }{ + {maxBlockHeight + 2, false}, + {10, true}, + {1, true}, + {maxBlockHeight + 100, false}, + } + + for { + time.Sleep(10 * time.Millisecond) + reactorPairs[1].conR.mtx.Lock() + if reactorPairs[1].conR.switchedToConsensus { + reactorPairs[1].conR.mtx.Unlock() + break + } + reactorPairs[1].conR.mtx.Unlock() + } + + assert.Equal(t, maxBlockHeight, reactorPairs[0].bcR.store.Height()) + + for _, tt := range tests { + block := reactorPairs[1].bcR.store.LoadBlock(tt.height) + if tt.existent { + assert.True(t, block != nil) + } else { + assert.True(t, block == nil) + } + } +} + +// NOTE: This is too hard to test without +// an easy way to add test peer to switch +// or without significant refactoring of the module. +// Alternatively we could actually dial a TCP conn but +// that seems extreme. +func TestFastSyncBadBlockStopsPeer(t *testing.T) { + numNodes := 4 + maxBlockHeight := int64(148) + + config = cfg.ResetTestRoot("blockchain_reactor_test") + defer os.RemoveAll(config.RootDir) + genDoc, privVals := randGenesisDoc(1, false, 30) + + otherChain := newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, maxBlockHeight) + defer func() { + _ = otherChain.bcR.Stop() + _ = otherChain.conR.Stop() + }() + + reactorPairs := make([]BlockchainReactorPair, numNodes) + logger := make([]log.Logger, numNodes) + + for i := 0; i < numNodes; i++ { + logger[i] = log.TestingLogger() + height := int64(0) + if i == 0 { + height = maxBlockHeight + } + reactorPairs[i] = newBlockchainReactorPair(logger[i], genDoc, privVals, height) + } + + switches := p2p.MakeConnectedSwitches(config.P2P, numNodes, func(i int, s *p2p.Switch) *p2p.Switch { + reactorPairs[i].conR.mtx.Lock() + s.AddReactor("BLOCKCHAIN", reactorPairs[i].bcR) + s.AddReactor("CONSENSUS", reactorPairs[i].conR) + moduleName := fmt.Sprintf("blockchain-%v", i) + reactorPairs[i].bcR.SetLogger(logger[i].With("module", moduleName)) + reactorPairs[i].conR.mtx.Unlock() + return s + + }, p2p.Connect2Switches) + + defer func() { + for _, r := range reactorPairs { + _ = r.bcR.Stop() + _ = r.conR.Stop() + } + }() + +outerFor: + for { + time.Sleep(10 * time.Millisecond) + for i := 0; i < numNodes; i++ { + reactorPairs[i].conR.mtx.Lock() + if !reactorPairs[i].conR.switchedToConsensus { + reactorPairs[i].conR.mtx.Unlock() + continue outerFor + } + reactorPairs[i].conR.mtx.Unlock() + } + break + } + + //at this time, reactors[0-3] is the newest + assert.Equal(t, numNodes-1, reactorPairs[1].bcR.Switch.Peers().Size()) + + //mark last reactorPair as an invalid peer + reactorPairs[numNodes-1].bcR.store = otherChain.bcR.store + + lastLogger := log.TestingLogger() + lastReactorPair := newBlockchainReactorPair(lastLogger, genDoc, privVals, 0) + reactorPairs = append(reactorPairs, lastReactorPair) + + switches = append(switches, p2p.MakeConnectedSwitches(config.P2P, 1, func(i int, s *p2p.Switch) *p2p.Switch { + s.AddReactor("BLOCKCHAIN", reactorPairs[len(reactorPairs)-1].bcR) + s.AddReactor("CONSENSUS", reactorPairs[len(reactorPairs)-1].conR) + moduleName := fmt.Sprintf("blockchain-%v", len(reactorPairs)-1) + reactorPairs[len(reactorPairs)-1].bcR.SetLogger(lastLogger.With("module", moduleName)) + return s + + }, p2p.Connect2Switches)...) + + for i := 0; i < len(reactorPairs)-1; i++ { + p2p.Connect2Switches(switches, i, len(reactorPairs)-1) + } + + for { + time.Sleep(1 * time.Second) + lastReactorPair.conR.mtx.Lock() + if lastReactorPair.conR.switchedToConsensus { + lastReactorPair.conR.mtx.Unlock() + break + } + lastReactorPair.conR.mtx.Unlock() + + if lastReactorPair.bcR.Switch.Peers().Size() == 0 { + break + } + } + + assert.True(t, lastReactorPair.bcR.Switch.Peers().Size() < len(reactorPairs)-1) +} + +//---------------------------------------------- +// utility funcs + +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < 10; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) + return block +} + +type testApp struct { + abci.BaseApplication +} diff --git a/blockchain/v1/wire.go b/blockchain/v1/wire.go new file mode 100644 index 000000000..786584435 --- /dev/null +++ b/blockchain/v1/wire.go @@ -0,0 +1,13 @@ +package v1 + +import ( + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/types" +) + +var cdc = amino.NewCodec() + +func init() { + RegisterBlockchainMessages(cdc) + types.RegisterBlockAmino(cdc) +} diff --git a/cmd/tendermint/commands/run_node.go b/cmd/tendermint/commands/run_node.go index fa63b4944..70de9aba7 100644 --- a/cmd/tendermint/commands/run_node.go +++ b/cmd/tendermint/commands/run_node.go @@ -19,7 +19,7 @@ func AddNodeFlags(cmd *cobra.Command) { cmd.Flags().String("priv_validator_laddr", config.PrivValidatorListenAddr, "Socket address to listen on for connections from external priv_validator process") // node flags - cmd.Flags().Bool("fast_sync", config.FastSync, "Fast blockchain syncing") + cmd.Flags().Bool("fast_sync", config.FastSyncMode, "Fast blockchain syncing") // abci flags cmd.Flags().String("proxy_app", config.ProxyApp, "Proxy app address, or one of: 'kvstore', 'persistent_kvstore', 'counter', 'counter_serial' or 'noop' for local testing.") diff --git a/config/config.go b/config/config.go index 73c704681..b00702ce6 100644 --- a/config/config.go +++ b/config/config.go @@ -64,6 +64,7 @@ type Config struct { RPC *RPCConfig `mapstructure:"rpc"` P2P *P2PConfig `mapstructure:"p2p"` Mempool *MempoolConfig `mapstructure:"mempool"` + FastSync *FastSyncConfig `mapstructure:"fastsync"` Consensus *ConsensusConfig `mapstructure:"consensus"` TxIndex *TxIndexConfig `mapstructure:"tx_index"` Instrumentation *InstrumentationConfig `mapstructure:"instrumentation"` @@ -76,6 +77,7 @@ func DefaultConfig() *Config { RPC: DefaultRPCConfig(), P2P: DefaultP2PConfig(), Mempool: DefaultMempoolConfig(), + FastSync: DefaultFastSyncConfig(), Consensus: DefaultConsensusConfig(), TxIndex: DefaultTxIndexConfig(), Instrumentation: DefaultInstrumentationConfig(), @@ -89,6 +91,7 @@ func TestConfig() *Config { RPC: TestRPCConfig(), P2P: TestP2PConfig(), Mempool: TestMempoolConfig(), + FastSync: TestFastSyncConfig(), Consensus: TestConsensusConfig(), TxIndex: TestTxIndexConfig(), Instrumentation: TestInstrumentationConfig(), @@ -120,6 +123,9 @@ func (cfg *Config) ValidateBasic() error { if err := cfg.Mempool.ValidateBasic(); err != nil { return errors.Wrap(err, "Error in [mempool] section") } + if err := cfg.FastSync.ValidateBasic(); err != nil { + return errors.Wrap(err, "Error in [fastsync] section") + } if err := cfg.Consensus.ValidateBasic(); err != nil { return errors.Wrap(err, "Error in [consensus] section") } @@ -151,7 +157,7 @@ type BaseConfig struct { // If this node is many blocks behind the tip of the chain, FastSync // allows them to catchup quickly by downloading blocks in parallel // and verifying their commits - FastSync bool `mapstructure:"fast_sync"` + FastSyncMode bool `mapstructure:"fast_sync"` // Database backend: goleveldb | cleveldb | boltdb // * goleveldb (github.com/syndtr/goleveldb - most popular implementation) @@ -216,7 +222,7 @@ func DefaultBaseConfig() BaseConfig { LogLevel: DefaultPackageLogLevels(), LogFormat: LogFormatPlain, ProfListenAddress: "", - FastSync: true, + FastSyncMode: true, FilterPeers: false, DBBackend: "goleveldb", DBPath: "data", @@ -228,7 +234,7 @@ func TestBaseConfig() BaseConfig { cfg := DefaultBaseConfig() cfg.chainID = "tendermint_test" cfg.ProxyApp = "kvstore" - cfg.FastSync = false + cfg.FastSyncMode = false cfg.DBBackend = "memdb" return cfg } @@ -684,6 +690,38 @@ func (cfg *MempoolConfig) ValidateBasic() error { return nil } +//----------------------------------------------------------------------------- +// FastSyncConfig + +// FastSyncConfig defines the configuration for the Tendermint fast sync service +type FastSyncConfig struct { + Version string `mapstructure:"version"` +} + +// DefaultFastSyncConfig returns a default configuration for the fast sync service +func DefaultFastSyncConfig() *FastSyncConfig { + return &FastSyncConfig{ + Version: "v0", + } +} + +// TestFastSyncConfig returns a default configuration for the fast sync. +func TestFastSyncConfig() *FastSyncConfig { + return DefaultFastSyncConfig() +} + +// ValidateBasic performs basic validation. +func (cfg *FastSyncConfig) ValidateBasic() error { + switch cfg.Version { + case "v0": + return nil + case "v1": + return nil + default: + return fmt.Errorf("unknown fastsync version %s", cfg.Version) + } +} + //----------------------------------------------------------------------------- // ConsensusConfig diff --git a/config/toml.go b/config/toml.go index 58da57975..5679a1caf 100644 --- a/config/toml.go +++ b/config/toml.go @@ -79,7 +79,7 @@ moniker = "{{ .BaseConfig.Moniker }}" # If this node is many blocks behind the tip of the chain, FastSync # allows them to catchup quickly by downloading blocks in parallel # and verifying their commits -fast_sync = {{ .BaseConfig.FastSync }} +fast_sync = {{ .BaseConfig.FastSyncMode }} # Database backend: goleveldb | cleveldb | boltdb # * goleveldb (github.com/syndtr/goleveldb - most popular implementation) @@ -294,6 +294,14 @@ max_txs_bytes = {{ .Mempool.MaxTxsBytes }} # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = {{ .Mempool.CacheSize }} +##### fast sync configuration options ##### +[fastsync] + +# Fast Sync version to use: +# 1) "v0" (default) - the legacy fast sync implementation +# 2) "v1" - refactor of v0 version for better testability +version = "{{ .FastSync.Version }}" + # Limit the size of TxMessage max_msg_bytes = {{ .Mempool.MaxMsgBytes }} diff --git a/consensus/common_test.go b/consensus/common_test.go index 29db524ec..c9e250edd 100644 --- a/consensus/common_test.go +++ b/consensus/common_test.go @@ -20,7 +20,6 @@ import ( "github.com/tendermint/tendermint/abci/example/counter" "github.com/tendermint/tendermint/abci/example/kvstore" abci "github.com/tendermint/tendermint/abci/types" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cstypes "github.com/tendermint/tendermint/consensus/types" cmn "github.com/tendermint/tendermint/libs/common" @@ -31,6 +30,7 @@ import ( "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/privval" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" ) @@ -280,7 +280,7 @@ func newConsensusStateWithConfig(thisConfig *cfg.Config, state sm.State, pv type func newConsensusStateWithConfigAndBlockStore(thisConfig *cfg.Config, state sm.State, pv types.PrivValidator, app abci.Application, blockDB dbm.DB) *ConsensusState { // Get BlockStore - blockStore := bc.NewBlockStore(blockDB) + blockStore := store.NewBlockStore(blockDB) // one for mempool, one for consensus mtx := new(sync.Mutex) diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index b237da6b5..308c5532f 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -17,7 +17,6 @@ import ( abcicli "github.com/tendermint/tendermint/abci/client" "github.com/tendermint/tendermint/abci/example/kvstore" abci "github.com/tendermint/tendermint/abci/types" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" @@ -25,6 +24,7 @@ import ( "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/p2p/mock" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) @@ -133,7 +133,7 @@ func TestReactorWithEvidence(t *testing.T) { // css[i] = newConsensusStateWithConfig(thisConfig, state, privVals[i], app) blockDB := dbm.NewMemDB() - blockStore := bc.NewBlockStore(blockDB) + blockStore := store.NewBlockStore(blockDB) // one for mempool, one for consensus mtx := new(sync.Mutex) diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 5bb73484e..215627a5d 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -11,7 +11,6 @@ import ( "github.com/pkg/errors" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" @@ -19,6 +18,7 @@ import ( "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) @@ -280,7 +280,7 @@ func newConsensusStateForReplay(config cfg.BaseConfig, csConfig *cfg.ConsensusCo dbType := dbm.DBBackendType(config.DBBackend) // Get BlockStore blockStoreDB := dbm.NewDB("blockstore", dbType, config.DBDir()) - blockStore := bc.NewBlockStore(blockStoreDB) + blockStore := store.NewBlockStore(blockStoreDB) // Get State stateDB := dbm.NewDB("state", dbType, config.DBDir()) diff --git a/consensus/wal_generator.go b/consensus/wal_generator.go index 2faff27b5..f2e7bb310 100644 --- a/consensus/wal_generator.go +++ b/consensus/wal_generator.go @@ -12,7 +12,6 @@ import ( "github.com/pkg/errors" "github.com/tendermint/tendermint/abci/example/kvstore" - bc "github.com/tendermint/tendermint/blockchain" cfg "github.com/tendermint/tendermint/config" cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/db" @@ -21,6 +20,7 @@ import ( "github.com/tendermint/tendermint/privval" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) @@ -55,7 +55,8 @@ func WALGenerateNBlocks(t *testing.T, wr io.Writer, numBlocks int) (err error) { } state.Version.Consensus.App = kvstore.ProtocolVersion sm.SaveState(stateDB, state) - blockStore := bc.NewBlockStore(blockStoreDB) + blockStore := store.NewBlockStore(blockStoreDB) + proxyApp := proxy.NewAppConns(proxy.NewLocalClientCreator(app)) proxyApp.SetLogger(logger.With("module", "proxy")) if err := proxyApp.Start(); err != nil { diff --git a/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-datastructs.png b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-datastructs.png new file mode 100644 index 0000000000000000000000000000000000000000..1a92871a5bcebf2f88658623b82c6e49b9cd25bc GIT binary patch literal 44461 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 literal 0 HcmV?d00001 diff --git a/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-fsm.png b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-fsm.png new file mode 100644 index 0000000000000000000000000000000000000000..87d6fad931c46840ac2f47243e10f9b7978046af GIT binary patch literal 42091 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 S9i>1)ezKCEC91^?gZ>}K2PIPg literal 0 HcmV?d00001 diff --git a/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-goroutines.png b/docs/spec/reactors/block_sync/bcv1/img/bc-reactor-new-goroutines.png new file mode 100644 index 0000000000000000000000000000000000000000..ee853ea935cb2d7adf35d63fcf576b9df305e0f4 GIT binary patch literal 140946 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

vg`M)Wq8{7?F**9ln|l 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 E57oWiU;qFB literal 0 HcmV?d00001 diff --git a/docs/spec/reactors/block_sync/bcv1/impl-v1.md b/docs/spec/reactors/block_sync/bcv1/impl-v1.md new file mode 100644 index 000000000..0ffaaea69 --- /dev/null +++ b/docs/spec/reactors/block_sync/bcv1/impl-v1.md @@ -0,0 +1,237 @@ +# Blockchain Reactor v1 + +### Data Structures +The data structures used are illustrated below. + +![Data Structures](img/bc-reactor-new-datastructs.png) + +#### BlockchainReactor +- is a `p2p.BaseReactor`. +- has a `store.BlockStore` for persistence. +- executes blocks using an `sm.BlockExecutor`. +- starts the FSM and the `poolRoutine()`. +- relays the fast-sync responses and switch messages to the FSM. +- handles errors from the FSM and when necessarily reports them to the switch. +- implements the blockchain reactor interface used by the FSM to send requests, errors to the switch and state timer resets. +- registers all the concrete types and interfaces for serialisation. + +```go +type BlockchainReactor struct { + p2p.BaseReactor + + initialState sm.State // immutable + state sm.State + + blockExec *sm.BlockExecutor + store *store.BlockStore + + fastSync bool + + fsm *BcReactorFSM + blocksSynced int + + // Receive goroutine forwards messages to this channel to be processed in the context of the poolRoutine. + messagesForFSMCh chan bcReactorMessage + + // Switch goroutine may send RemovePeer to the blockchain reactor. This is an error message that is relayed + // to this channel to be processed in the context of the poolRoutine. + errorsForFSMCh chan bcReactorMessage + + // This channel is used by the FSM and indirectly the block pool to report errors to the blockchain reactor and + // the switch. + eventsFromFSMCh chan bcFsmMessage +} +``` + +#### BcReactorFSM +- implements a simple finite state machine. +- has a state and a state timer. +- has a `BlockPool` to keep track of block requests sent to peers and blocks received from peers. +- uses an interface to send status requests, block requests and reporting errors. The interface is implemented by the `BlockchainReactor` and tests. + +```go +type BcReactorFSM struct { + logger log.Logger + mtx sync.Mutex + + startTime time.Time + + state *bcReactorFSMState + stateTimer *time.Timer + pool *BlockPool + + // interface used to call the Blockchain reactor to send StatusRequest, BlockRequest, reporting errors, etc. + toBcR bcReactor +} +``` + +#### BlockPool +- maintains a peer set, implemented as a map of peer ID to `BpPeer`. +- maintains a set of requests made to peers, implemented as a map of block request heights to peer IDs. +- maintains a list of future block requests needed to advance the fast-sync. This is a list of block heights. +- keeps track of the maximum height of the peers in the set. +- uses an interface to send requests and report errors to the reactor (via FSM). + +```go +type BlockPool struct { + logger log.Logger + // Set of peers that have sent status responses, with height bigger than pool.Height + peers map[p2p.ID]*BpPeer + // Set of block heights and the corresponding peers from where a block response is expected or has been received. + blocks map[int64]p2p.ID + + plannedRequests map[int64]struct{} // list of blocks to be assigned peers for blockRequest + nextRequestHeight int64 // next height to be added to plannedRequests + + Height int64 // height of next block to execute + MaxPeerHeight int64 // maximum height of all peers + toBcR bcReactor +} +``` +Some reasons for the `BlockPool` data structure content: +1. If a peer is removed by the switch fast access is required to the peer and the block requests made to that peer in order to redo them. +2. When block verification fails fast access is required from the block height to the peer and the block requests made to that peer in order to redo them. +3. The `BlockchainReactor` main routine decides when the block pool is running low and asks the `BlockPool` (via FSM) to make more requests. The `BlockPool` creates a list of requests and triggers the sending of the block requests (via the interface). The reason it maintains a list of requests is the redo operations that may occur during error handling. These are redone when the `BlockchainReactor` requires more blocks. + +#### BpPeer +- keeps track of a single peer, with height bigger than the initial height. +- maintains the block requests made to the peer and the blocks received from the peer until they are executed. +- monitors the peer speed when there are pending requests. +- it has an active timer when pending requests are present and reports error on timeout. + +```go +type BpPeer struct { + logger log.Logger + ID p2p.ID + + Height int64 // the peer reported height + NumPendingBlockRequests int // number of requests still waiting for block responses + blocks map[int64]*types.Block // blocks received or expected to be received from this peer + blockResponseTimer *time.Timer + recvMonitor *flow.Monitor + params *BpPeerParams // parameters for timer and monitor + + onErr func(err error, peerID p2p.ID) // function to call on error +} +``` + +### Concurrency Model + +The diagram below shows the goroutines (depicted by the gray blocks), timers (shown on the left with their values) and channels (colored rectangles). The FSM box shows some of the functionality and it is not a separate goroutine. + +The interface used by the FSM is shown in light red with the `IF` block. This is used to: +- send block requests +- report peer errors to the switch - this results in the reactor calling `switch.StopPeerForError()` and, if triggered by the peer timeout routine, a `removePeerEv` is sent to the FSM and action is taken from the context of the `poolRoutine()` +- ask the reactor to reset the state timers. The timers are owned by the FSM while the timeout routine is defined by the reactor. This was done in order to avoid running timers in tests and will change in the next revision. + +There are two main goroutines implemented by the blockchain reactor. All I/O operations are performed from the `poolRoutine()` context while the CPU intensive operations related to the block execution are performed from the context of the `executeBlocksRoutine()`. All goroutines are detailed in the next sections. + +![Go Routines Diagram](img/bc-reactor-new-goroutines.png) + +#### Receive() +Fast-sync messages from peers are received by this goroutine. It performs basic validation and: +- in helper mode (i.e. for request message) it replies immediately. This is different than the proposal in adr-040 that specifies having the FSM handling these. +- forwards response messages to the `poolRoutine()`. + +#### poolRoutine() +(named kept as in the previous reactor). +It starts the `executeBlocksRoutine()` and the FSM. It then waits in a loop for events. These are received from the following channels: +- `sendBlockRequestTicker.C` - every 10msec the reactor asks FSM to make more block requests up to a maximum. Note: currently this value is constant but could be changed based on low/ high watermark thresholds for the number of blocks received and waiting to be processed, the number of blockResponse messages waiting in messagesForFSMCh, etc. +- `statusUpdateTicker.C` - every 10 seconds the reactor broadcasts status requests to peers. While adr-040 specifies this to run within the FSM, at this point this functionality is kept in the reactor. +- `messagesForFSMCh` - the `Receive()` goroutine sends status and block response messages to this channel and the reactor calls FSM to handle them. +- `errorsForFSMCh` - this channel receives the following events: + - peer remove - when the switch removes a peer + - sate timeout event - when FSM state timers trigger + The reactor forwards this messages to the FSM. +- `eventsFromFSMCh` - there are two type of events sent over this channel: + - `syncFinishedEv` - triggered when FSM enters `finished` state and calls the switchToConsensus() interface function. + - `peerErrorEv`- peer timer expiry goroutine sends this event over the channel for processing from poolRoutine() context. + +#### executeBlocksRoutine() +Started by the `poolRoutine()`, it retrieves blocks from the pool and executes them: +- `processReceivedBlockTicker.C` - a ticker event is received over the channel every 10msec and its handling results in a signal being sent to the doProcessBlockCh channel. +- doProcessBlockCh - events are received on this channel as described as above and upon processing blocks are retrieved from the pool and executed. + + +### FSM + +![fsm](img/bc-reactor-new-fsm.png) + +#### States +##### init (aka unknown) +The FSM is created in `unknown` state. When started, by the reactor (`startFSMEv`), it broadcasts Status requests and transitions to `waitForPeer` state. + +##### waitForPeer +In this state, the FSM waits for a Status responses from a "tall" peer. A timer is running in this state to allow the FSM to finish if there are no useful peers. + +If the timer expires, it moves to `finished` state and calls the reactor to switch to consensus. +If a Status response is received from a peer within the timeout, the FSM transitions to `waitForBlock` state. + +##### waitForBlock +In this state the FSM makes Block requests (triggered by a ticker in reactor) and waits for Block responses. There is a timer running in this state to detect if a peer is not sending the block at current processing height. If the timer expires, the FSM removes the peer where the request was sent and all requests made to that peer are redone. + +As blocks are received they are stored by the pool. Block execution is independently performed by the reactor and the result reported to the FSM: +- if there are no errors, the FSM increases the pool height and resets the state timer. +- if there are errors, the peers that delivered the two blocks (at height and height+1) are removed and the requests redone. + +In this state the FSM may receive peer remove events in any of the following scenarios: +- the switch is removing a peer +- a peer is penalized because it has not responded to some block requests for a long time +- a peer is penalized for being slow + +When processing of the last block (the one with height equal to the highest peer height minus one) is successful, the FSM transitions to `finished` state. +If after a peer update or removal the pool height is same as maxPeerHeight, the FSM transitions to `finished` state. + +##### finished +When entering this state, the FSM calls the reactor to switch to consensus and performs cleanup. + +#### Events + +The following events are handled by the FSM: + +```go +const ( + startFSMEv = iota + 1 + statusResponseEv + blockResponseEv + processedBlockEv + makeRequestsEv + stopFSMEv + peerRemoveEv = iota + 256 + stateTimeoutEv +) +``` + +### Examples of Scenarios and Termination Handling +A few scenarios are covered in this section together with the current/ proposed handling. +In general, the scenarios involving faulty peers are made worse by the fact that they may quickly be re-added. + +#### 1. No Tall Peers + +S: In this scenario a node is started and while there are status responses received, none of the peers are at a height higher than this node. + +H: The FSM times out in `waitForPeer` state, moves to `finished` state where it calls the reactor to switch to consensus. + +#### 2. Typical Fast Sync + +S: A node fast syncs blocks from honest peers and eventually downloads and executes the penultimate block. + +H: The FSM in `waitForBlock` state will receive the processedBlockEv from the reactor and detect that the termination height is achieved. + +#### 3. Peer Claims Big Height but no Blocks + +S: In this scenario a faulty peer claims a big height (for which there are no blocks). + +H: The requests for the non-existing block will timeout, the peer removed and the pool's `MaxPeerHeight` updated. FSM checks if the termination height is achieved when peers are removed. + +#### 4. Highest Peer Removed or Updated to Short + +S: The fast sync node is caught up with all peers except one tall peer. The tall peer is removed or it sends status response with low height. + +H: FSM checks termination condition on peer removal and updates. + +#### 5. Block At Current Height Delayed + +S: A peer can block the progress of fast sync by delaying indefinitely the block response for the current processing height (h1). + +H: Currently, given h1 < h2, there is no enforcement at peer level that the response for h1 should be received before h2. So a peer will timeout only after delivering all blocks except h1. However the `waitForBlock` state timer fires if the block for current processing height is not received within a timeout. The peer is removed and the requests to that peer (including the one for current height) redone. diff --git a/docs/spec/reactors/block_sync/img/bc-reactor-routines.png b/docs/spec/reactors/block_sync/img/bc-reactor-routines.png new file mode 100644 index 0000000000000000000000000000000000000000..3f574a79b1ad304e7c03cfdb717c4f9aa600359a GIT binary patch literal 271695 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 z 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 abc30821... blockchain: Reorg reactor (#3561) github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= @@ -75,8 +80,13 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= +<<<<<<< HEAD github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +======= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +>>>>>>> abc30821... blockchain: Reorg reactor (#3561) github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno= @@ -115,8 +125,14 @@ github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYM go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +<<<<<<< HEAD golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +======= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +>>>>>>> abc30821... blockchain: Reorg reactor (#3561) golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= @@ -127,11 +143,14 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 h1:67iHsV9djwGdZpdZNbLuQj6FOzCaZe3w+vhLjn5AcFA= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.13.0 h1:bHIbVsCwmvbArgCJmLdgOdHFXlKqTOVjbibbS19cXHc= -google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= +google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= +google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= diff --git a/node/node.go b/node/node.go index 791444b4a..9ccf57691 100644 --- a/node/node.go +++ b/node/node.go @@ -18,8 +18,8 @@ import ( amino "github.com/tendermint/go-amino" abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/blockchain" - bc "github.com/tendermint/tendermint/blockchain" + bcv0 "github.com/tendermint/tendermint/blockchain/v0" + bcv1 "github.com/tendermint/tendermint/blockchain/v1" cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/consensus" cs "github.com/tendermint/tendermint/consensus" @@ -42,6 +42,7 @@ import ( "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/state/txindex/kv" "github.com/tendermint/tendermint/state/txindex/null" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" @@ -175,9 +176,9 @@ type Node struct { // services eventBus *types.EventBus // pub/sub for services stateDB dbm.DB - blockStore *bc.BlockStore // store the blockchain to disk - bcReactor *bc.BlockchainReactor // for fast-syncing - mempoolReactor *mempl.Reactor // for gossipping transactions + blockStore *store.BlockStore // store the blockchain to disk + bcReactor p2p.Reactor // for fast-syncing + mempoolReactor *mempl.Reactor // for gossipping transactions mempool mempl.Mempool consensusState *cs.ConsensusState // latest consensus state consensusReactor *cs.ConsensusReactor // for participating in the consensus @@ -190,13 +191,13 @@ type Node struct { prometheusSrv *http.Server } -func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *bc.BlockStore, stateDB dbm.DB, err error) { +func initDBs(config *cfg.Config, dbProvider DBProvider) (blockStore *store.BlockStore, stateDB dbm.DB, err error) { var blockStoreDB dbm.DB blockStoreDB, err = dbProvider(&DBContext{"blockstore", config}) if err != nil { return } - blockStore = bc.NewBlockStore(blockStoreDB) + blockStore = store.NewBlockStore(blockStoreDB) stateDB, err = dbProvider(&DBContext{"state", config}) if err != nil { @@ -337,6 +338,26 @@ func createEvidenceReactor(config *cfg.Config, dbProvider DBProvider, return evidenceReactor, evidencePool, nil } +func createBlockchainReactor(config *cfg.Config, + state sm.State, + blockExec *sm.BlockExecutor, + blockStore *store.BlockStore, + fastSync bool, + logger log.Logger) (bcReactor p2p.Reactor, err error) { + + switch config.FastSync.Version { + case "v0": + bcReactor = bcv0.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + case "v1": + bcReactor = bcv1.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + default: + return nil, fmt.Errorf("unknown fastsync version %s", config.FastSync.Version) + } + + bcReactor.SetLogger(logger.With("module", "blockchain")) + return bcReactor, nil +} + func createConsensusReactor(config *cfg.Config, state sm.State, blockExec *sm.BlockExecutor, @@ -431,7 +452,7 @@ func createSwitch(config *cfg.Config, p2pMetrics *p2p.Metrics, peerFilters []p2p.PeerFilterFunc, mempoolReactor *mempl.Reactor, - bcReactor *blockchain.BlockchainReactor, + bcReactor p2p.Reactor, consensusReactor *consensus.ConsensusReactor, evidenceReactor *evidence.EvidenceReactor, nodeInfo p2p.NodeInfo, @@ -572,7 +593,7 @@ func NewNode(config *cfg.Config, // Decide whether to fast-sync or not // We don't fast-sync when the only validator is us. - fastSync := config.FastSync && !onlyValidatorIsUs(state, privValidator) + fastSync := config.FastSyncMode && !onlyValidatorIsUs(state, privValidator) csMetrics, p2pMetrics, memplMetrics, smMetrics := metricsProvider(genDoc.ChainID) @@ -596,8 +617,10 @@ func NewNode(config *cfg.Config, ) // Make BlockchainReactor - bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) - bcReactor.SetLogger(logger.With("module", "blockchain")) + bcReactor, err := createBlockchainReactor(config, state, blockExec, blockStore, fastSync, logger) + if err != nil { + return nil, errors.Wrap(err, "could not create blockchain reactor") + } // Make ConsensusReactor consensusReactor, consensusState := createConsensusReactor( @@ -930,7 +953,7 @@ func (n *Node) Switch() *p2p.Switch { } // BlockStore returns the Node's BlockStore. -func (n *Node) BlockStore() *bc.BlockStore { +func (n *Node) BlockStore() *store.BlockStore { return n.blockStore } @@ -1018,6 +1041,17 @@ func makeNodeInfo( if _, ok := txIndexer.(*null.TxIndex); ok { txIndexerStatus = "off" } + + var bcChannel byte + switch config.FastSync.Version { + case "v0": + bcChannel = bcv0.BlockchainChannel + case "v1": + bcChannel = bcv1.BlockchainChannel + default: + return nil, fmt.Errorf("unknown fastsync version %s", config.FastSync.Version) + } + nodeInfo := p2p.DefaultNodeInfo{ ProtocolVersion: p2p.NewProtocolVersion( version.P2PProtocol, // global @@ -1028,7 +1062,7 @@ func makeNodeInfo( Network: genDoc.ChainID, Version: version.TMCoreSemVer, Channels: []byte{ - bc.BlockchainChannel, + bcChannel, cs.StateChannel, cs.DataChannel, cs.VoteChannel, cs.VoteSetBitsChannel, mempl.MempoolChannel, evidence.EvidenceChannel, diff --git a/blockchain/store.go b/store/store.go similarity index 98% rename from blockchain/store.go rename to store/store.go index b7f4e07c8..7375c8481 100644 --- a/blockchain/store.go +++ b/store/store.go @@ -1,4 +1,4 @@ -package blockchain +package store import ( "fmt" @@ -216,6 +216,7 @@ func calcSeenCommitKey(height int64) []byte { var blockStoreKey = []byte("blockStore") +// BlockStoreStateJSON is the block store state JSON structure. type BlockStoreStateJSON struct { Height int64 `json:"height"` } diff --git a/blockchain/store_test.go b/store/store_test.go similarity index 97% rename from blockchain/store_test.go rename to store/store_test.go index bd30bc6d2..974c38045 100644 --- a/blockchain/store_test.go +++ b/store/store_test.go @@ -1,4 +1,4 @@ -package blockchain +package store import ( "bytes" @@ -32,6 +32,18 @@ func makeTestCommit(height int64, timestamp time.Time) *types.Commit { return types.NewCommit(types.BlockID{}, commitSigs) } +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < 10; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) + return block +} + func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFunc) { config := cfg.ResetTestRoot("blockchain_reactor_test") // blockDB := dbm.NewDebugDB("blockDB", dbm.NewMemDB()) diff --git a/store/wire.go b/store/wire.go new file mode 100644 index 000000000..67b838c01 --- /dev/null +++ b/store/wire.go @@ -0,0 +1,12 @@ +package store + +import ( + amino "github.com/tendermint/go-amino" + cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" +) + +var cdc = amino.NewCodec() + +func init() { + cryptoAmino.RegisterAmino(cdc) +} From 717aa4400469e75b0deeb1a9adf53c1d46d16de9 Mon Sep 17 00:00:00 2001 From: Marko Date: Tue, 23 Jul 2019 15:35:36 +0200 Subject: [PATCH 150/211] Renamed wire.go to codec.go (#3827) * Renamed wire.go to codec.go - Wire was the previous name of amino - Codec describes the file better than `wire` & `amino` Signed-off-by: Marko Baricevic * ide error * rename amino.go to codec.go --- blockchain/v0/{wire.go => codec.go} | 0 blockchain/v1/{wire.go => codec.go} | 0 cmd/tendermint/commands/{wire.go => codec.go} | 0 consensus/{wire.go => codec.go} | 0 consensus/types/{wire.go => codec.go} | 0 crypto/merkle/{wire.go => codec.go} | 0 crypto/multisig/{wire.go => codec.go} | 0 evidence/{wire.go => codec.go} | 0 mempool/{wire.go => codec.go} | 0 node/{wire.go => codec.go} | 0 p2p/{wire.go => codec.go} | 0 p2p/conn/{wire.go => codec.go} | 0 p2p/pex/{wire.go => codec.go} | 0 privval/{wire.go => codec.go} | 0 rpc/client/{amino.go => codec.go} | 0 rpc/core/types/{wire.go => codec.go} | 0 state/{wire.go => codec.go} | 0 state/txindex/kv/{wire.go => codec.go} | 0 store/{wire.go => codec.go} | 0 tools/tm-monitor/{wire.go => codec.go} | 0 tools/tm-monitor/monitor/{wire.go => codec.go} | 0 types/{wire.go => codec.go} | 0 22 files changed, 0 insertions(+), 0 deletions(-) rename blockchain/v0/{wire.go => codec.go} (100%) rename blockchain/v1/{wire.go => codec.go} (100%) rename cmd/tendermint/commands/{wire.go => codec.go} (100%) rename consensus/{wire.go => codec.go} (100%) rename consensus/types/{wire.go => codec.go} (100%) rename crypto/merkle/{wire.go => codec.go} (100%) rename crypto/multisig/{wire.go => codec.go} (100%) rename evidence/{wire.go => codec.go} (100%) rename mempool/{wire.go => codec.go} (100%) rename node/{wire.go => codec.go} (100%) rename p2p/{wire.go => codec.go} (100%) rename p2p/conn/{wire.go => codec.go} (100%) rename p2p/pex/{wire.go => codec.go} (100%) rename privval/{wire.go => codec.go} (100%) rename rpc/client/{amino.go => codec.go} (100%) rename rpc/core/types/{wire.go => codec.go} (100%) rename state/{wire.go => codec.go} (100%) rename state/txindex/kv/{wire.go => codec.go} (100%) rename store/{wire.go => codec.go} (100%) rename tools/tm-monitor/{wire.go => codec.go} (100%) rename tools/tm-monitor/monitor/{wire.go => codec.go} (100%) rename types/{wire.go => codec.go} (100%) diff --git a/blockchain/v0/wire.go b/blockchain/v0/codec.go similarity index 100% rename from blockchain/v0/wire.go rename to blockchain/v0/codec.go diff --git a/blockchain/v1/wire.go b/blockchain/v1/codec.go similarity index 100% rename from blockchain/v1/wire.go rename to blockchain/v1/codec.go diff --git a/cmd/tendermint/commands/wire.go b/cmd/tendermint/commands/codec.go similarity index 100% rename from cmd/tendermint/commands/wire.go rename to cmd/tendermint/commands/codec.go diff --git a/consensus/wire.go b/consensus/codec.go similarity index 100% rename from consensus/wire.go rename to consensus/codec.go diff --git a/consensus/types/wire.go b/consensus/types/codec.go similarity index 100% rename from consensus/types/wire.go rename to consensus/types/codec.go diff --git a/crypto/merkle/wire.go b/crypto/merkle/codec.go similarity index 100% rename from crypto/merkle/wire.go rename to crypto/merkle/codec.go diff --git a/crypto/multisig/wire.go b/crypto/multisig/codec.go similarity index 100% rename from crypto/multisig/wire.go rename to crypto/multisig/codec.go diff --git a/evidence/wire.go b/evidence/codec.go similarity index 100% rename from evidence/wire.go rename to evidence/codec.go diff --git a/mempool/wire.go b/mempool/codec.go similarity index 100% rename from mempool/wire.go rename to mempool/codec.go diff --git a/node/wire.go b/node/codec.go similarity index 100% rename from node/wire.go rename to node/codec.go diff --git a/p2p/wire.go b/p2p/codec.go similarity index 100% rename from p2p/wire.go rename to p2p/codec.go diff --git a/p2p/conn/wire.go b/p2p/conn/codec.go similarity index 100% rename from p2p/conn/wire.go rename to p2p/conn/codec.go diff --git a/p2p/pex/wire.go b/p2p/pex/codec.go similarity index 100% rename from p2p/pex/wire.go rename to p2p/pex/codec.go diff --git a/privval/wire.go b/privval/codec.go similarity index 100% rename from privval/wire.go rename to privval/codec.go diff --git a/rpc/client/amino.go b/rpc/client/codec.go similarity index 100% rename from rpc/client/amino.go rename to rpc/client/codec.go diff --git a/rpc/core/types/wire.go b/rpc/core/types/codec.go similarity index 100% rename from rpc/core/types/wire.go rename to rpc/core/types/codec.go diff --git a/state/wire.go b/state/codec.go similarity index 100% rename from state/wire.go rename to state/codec.go diff --git a/state/txindex/kv/wire.go b/state/txindex/kv/codec.go similarity index 100% rename from state/txindex/kv/wire.go rename to state/txindex/kv/codec.go diff --git a/store/wire.go b/store/codec.go similarity index 100% rename from store/wire.go rename to store/codec.go diff --git a/tools/tm-monitor/wire.go b/tools/tm-monitor/codec.go similarity index 100% rename from tools/tm-monitor/wire.go rename to tools/tm-monitor/codec.go diff --git a/tools/tm-monitor/monitor/wire.go b/tools/tm-monitor/monitor/codec.go similarity index 100% rename from tools/tm-monitor/monitor/wire.go rename to tools/tm-monitor/monitor/codec.go diff --git a/types/wire.go b/types/codec.go similarity index 100% rename from types/wire.go rename to types/codec.go From c9f681d49576b2d51bd28f06cdcc7fddcb63d40b Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 24 Jul 2019 23:05:00 +0400 Subject: [PATCH 151/211] docs: add guides to docs (#3830) --- docs/.vuepress/config.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js index 9adfc5953..70b404c42 100644 --- a/docs/.vuepress/config.js +++ b/docs/.vuepress/config.js @@ -34,6 +34,14 @@ module.exports = { "/introduction/what-is-tendermint" ] }, + { + title: "Guides", + collapsable: false, + children: [ + "/guides/go-built-in", + "/guides/go" + ] + }, { title: "Apps", collapsable: false, From 96ebf60628cfba6d557bb3a99eab92422b8dcef1 Mon Sep 17 00:00:00 2001 From: Marko Date: Thu, 25 Jul 2019 07:35:30 +0200 Subject: [PATCH 152/211] add staticcheck linting (#3828) cleanup to add linter grpc change: https://godoc.org/google.golang.org/grpc#WithContextDialer https://godoc.org/google.golang.org/grpc#WithDialer grpc/grpc-go#2627 prometheous change: due to UninstrumentedHandler, being deprecated in the future empty branch = empty if or else statement didn't delete them entirely but commented couldn't find a reason to have them could not replicate the issue #3406 but if want to keep it commented then we should comment out the if statement as well --- .golangci.yml | 1 - abci/client/grpc_client.go | 32 +++++++++++------------ abci/example/example_test.go | 4 +-- blockchain/v0/reactor.go | 20 ++++++-------- blockchain/v1/reactor.go | 18 +++++++------ consensus/mempool_test.go | 12 ++++++--- consensus/reactor_test.go | 2 +- consensus/state.go | 21 ++++++++------- consensus/state_test.go | 6 ----- go.sum | 1 + libs/common/async.go | 10 ++++--- libs/common/async_test.go | 5 ++-- libs/pubsub/pubsub_test.go | 4 +-- lite/proxy/query_test.go | 4 +-- mempool/clist_mempool.go | 10 +++---- p2p/conn/connection_test.go | 3 ++- p2p/conn/secret_connection_test.go | 12 ++++++--- p2p/switch_test.go | 4 +-- privval/signer_validator_endpoint_test.go | 3 ++- rpc/grpc/client_server.go | 6 ++--- rpc/lib/client/ws_client.go | 18 +++++++------ rpc/lib/client/ws_client_test.go | 3 ++- state/state_test.go | 4 +-- tools/tm-monitor/mock/eventmeter.go | 2 +- types/genesis_test.go | 2 +- types/validator_set.go | 14 +++++----- 26 files changed, 116 insertions(+), 105 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index 6adbbd9da..b07ec3a46 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -8,7 +8,6 @@ linters: - golint - maligned - errcheck - - staticcheck - interfacer - unconvert - goconst diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index 8c444abc5..e326055fb 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -6,8 +6,8 @@ import ( "sync" "time" - context "golang.org/x/net/context" - grpc "google.golang.org/grpc" + "golang.org/x/net/context" + "google.golang.org/grpc" "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" @@ -39,7 +39,7 @@ func NewGRPCClient(addr string, mustConnect bool) *grpcClient { return cli } -func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { +func dialerFunc(ctx context.Context, addr string) (net.Conn, error) { return cmn.Connect(addr) } @@ -49,7 +49,7 @@ func (cli *grpcClient) OnStart() error { } RETRY_LOOP: for { - conn, err := grpc.Dial(cli.addr, grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) + conn, err := grpc.Dial(cli.addr, grpc.WithInsecure(), grpc.WithContextDialer(dialerFunc)) if err != nil { if cli.mustConnect { return err @@ -65,7 +65,7 @@ RETRY_LOOP: ENSURE_CONNECTED: for { - _, err := client.Echo(context.Background(), &types.RequestEcho{Message: "hello"}, grpc.FailFast(true)) + _, err := client.Echo(context.Background(), &types.RequestEcho{Message: "hello"}, grpc.WaitForReady(true)) if err == nil { break ENSURE_CONNECTED } @@ -125,7 +125,7 @@ func (cli *grpcClient) SetResponseCallback(resCb Callback) { func (cli *grpcClient) EchoAsync(msg string) *ReqRes { req := types.ToRequestEcho(msg) - res, err := cli.client.Echo(context.Background(), req.GetEcho(), grpc.FailFast(true)) + res, err := cli.client.Echo(context.Background(), req.GetEcho(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -134,7 +134,7 @@ func (cli *grpcClient) EchoAsync(msg string) *ReqRes { func (cli *grpcClient) FlushAsync() *ReqRes { req := types.ToRequestFlush() - res, err := cli.client.Flush(context.Background(), req.GetFlush(), grpc.FailFast(true)) + res, err := cli.client.Flush(context.Background(), req.GetFlush(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -143,7 +143,7 @@ func (cli *grpcClient) FlushAsync() *ReqRes { func (cli *grpcClient) InfoAsync(params types.RequestInfo) *ReqRes { req := types.ToRequestInfo(params) - res, err := cli.client.Info(context.Background(), req.GetInfo(), grpc.FailFast(true)) + res, err := cli.client.Info(context.Background(), req.GetInfo(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -152,7 +152,7 @@ func (cli *grpcClient) InfoAsync(params types.RequestInfo) *ReqRes { func (cli *grpcClient) SetOptionAsync(params types.RequestSetOption) *ReqRes { req := types.ToRequestSetOption(params) - res, err := cli.client.SetOption(context.Background(), req.GetSetOption(), grpc.FailFast(true)) + res, err := cli.client.SetOption(context.Background(), req.GetSetOption(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -161,7 +161,7 @@ func (cli *grpcClient) SetOptionAsync(params types.RequestSetOption) *ReqRes { func (cli *grpcClient) DeliverTxAsync(params types.RequestDeliverTx) *ReqRes { req := types.ToRequestDeliverTx(params) - res, err := cli.client.DeliverTx(context.Background(), req.GetDeliverTx(), grpc.FailFast(true)) + res, err := cli.client.DeliverTx(context.Background(), req.GetDeliverTx(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -170,7 +170,7 @@ func (cli *grpcClient) DeliverTxAsync(params types.RequestDeliverTx) *ReqRes { func (cli *grpcClient) CheckTxAsync(params types.RequestCheckTx) *ReqRes { req := types.ToRequestCheckTx(params) - res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.FailFast(true)) + res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -179,7 +179,7 @@ func (cli *grpcClient) CheckTxAsync(params types.RequestCheckTx) *ReqRes { func (cli *grpcClient) QueryAsync(params types.RequestQuery) *ReqRes { req := types.ToRequestQuery(params) - res, err := cli.client.Query(context.Background(), req.GetQuery(), grpc.FailFast(true)) + res, err := cli.client.Query(context.Background(), req.GetQuery(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -188,7 +188,7 @@ func (cli *grpcClient) QueryAsync(params types.RequestQuery) *ReqRes { func (cli *grpcClient) CommitAsync() *ReqRes { req := types.ToRequestCommit() - res, err := cli.client.Commit(context.Background(), req.GetCommit(), grpc.FailFast(true)) + res, err := cli.client.Commit(context.Background(), req.GetCommit(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -197,7 +197,7 @@ func (cli *grpcClient) CommitAsync() *ReqRes { func (cli *grpcClient) InitChainAsync(params types.RequestInitChain) *ReqRes { req := types.ToRequestInitChain(params) - res, err := cli.client.InitChain(context.Background(), req.GetInitChain(), grpc.FailFast(true)) + res, err := cli.client.InitChain(context.Background(), req.GetInitChain(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -206,7 +206,7 @@ func (cli *grpcClient) InitChainAsync(params types.RequestInitChain) *ReqRes { func (cli *grpcClient) BeginBlockAsync(params types.RequestBeginBlock) *ReqRes { req := types.ToRequestBeginBlock(params) - res, err := cli.client.BeginBlock(context.Background(), req.GetBeginBlock(), grpc.FailFast(true)) + res, err := cli.client.BeginBlock(context.Background(), req.GetBeginBlock(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } @@ -215,7 +215,7 @@ func (cli *grpcClient) BeginBlockAsync(params types.RequestBeginBlock) *ReqRes { func (cli *grpcClient) EndBlockAsync(params types.RequestEndBlock) *ReqRes { req := types.ToRequestEndBlock(params) - res, err := cli.client.EndBlock(context.Background(), req.GetEndBlock(), grpc.FailFast(true)) + res, err := cli.client.EndBlock(context.Background(), req.GetEndBlock(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } diff --git a/abci/example/example_test.go b/abci/example/example_test.go index 6282f3a44..74510700b 100644 --- a/abci/example/example_test.go +++ b/abci/example/example_test.go @@ -107,7 +107,7 @@ func testStream(t *testing.T, app types.Application) { //------------------------- // test grpc -func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { +func dialerFunc(ctx context.Context, addr string) (net.Conn, error) { return cmn.Connect(addr) } @@ -123,7 +123,7 @@ func testGRPCSync(t *testing.T, app *types.GRPCApplication) { defer server.Stop() // Connect to the socket - conn, err := grpc.Dial("unix://test.sock", grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) + conn, err := grpc.Dial("unix://test.sock", grpc.WithInsecure(), grpc.WithContextDialer(dialerFunc)) if err != nil { t.Fatalf("Error dialing GRPC server: %v", err.Error()) } diff --git a/blockchain/v0/reactor.go b/blockchain/v0/reactor.go index 5d38471dc..574ef3f29 100644 --- a/blockchain/v0/reactor.go +++ b/blockchain/v0/reactor.go @@ -141,9 +141,9 @@ func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { // AddPeer implements Reactor by sending our state to peer. func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) - if !peer.Send(BlockchainChannel, msgBytes) { - // doing nothing, will try later in `poolRoutine` - } + peer.Send(BlockchainChannel, msgBytes) + // it's OK if send fails. will try later in poolRoutine + // peer is added to the pool once we receive the first // bcStatusResponseMessage from the peer and call pool.SetPeerHeight } @@ -191,18 +191,13 @@ func (bcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) switch msg := msg.(type) { case *bcBlockRequestMessage: - if queued := bcR.respondToPeer(msg, src); !queued { - // Unfortunately not queued since the queue is full. - } + bcR.respondToPeer(msg, src) case *bcBlockResponseMessage: bcR.pool.AddBlock(src.ID(), msg.Block, len(msgBytes)) case *bcStatusRequestMessage: // Send peer our state. msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) - queued := src.TrySend(BlockchainChannel, msgBytes) - if !queued { - // sorry - } + src.TrySend(BlockchainChannel, msgBytes) case *bcStatusResponseMessage: // Got a peer status. Unverified. bcR.pool.SetPeerHeight(src.ID(), msg.Height) @@ -274,9 +269,10 @@ FOR_LOOP: conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) if ok { conR.SwitchToConsensus(state, blocksSynced) - } else { - // should only happen during testing } + // else { + // should only happen during testing + // } break FOR_LOOP } diff --git a/blockchain/v1/reactor.go b/blockchain/v1/reactor.go index 2f95cebaf..480b87f34 100644 --- a/blockchain/v1/reactor.go +++ b/blockchain/v1/reactor.go @@ -169,9 +169,9 @@ func (bcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { // AddPeer implements Reactor by sending our state to peer. func (bcR *BlockchainReactor) AddPeer(peer p2p.Peer) { msgBytes := cdc.MustMarshalBinaryBare(&bcStatusResponseMessage{bcR.store.Height()}) - if !peer.Send(BlockchainChannel, msgBytes) { - // doing nothing, will try later in `poolRoutine` - } + peer.Send(BlockchainChannel, msgBytes) + // it's OK if send fails. will try later in poolRoutine + // peer is added to the pool once we receive the first // bcStatusResponseMessage from the peer and call pool.updatePeer() } @@ -381,10 +381,11 @@ ForLoop: err: msg.data.err, }, }) - } else { - // For slow peers, or errors due to blocks received from wrong peer - // the FSM had already removed the peers } + // else { + // For slow peers, or errors due to blocks received from wrong peer + // the FSM had already removed the peers + // } default: bcR.Logger.Error("Event from FSM not supported", "type", msg.event) } @@ -465,9 +466,10 @@ func (bcR *BlockchainReactor) switchToConsensus() { if ok { conR.SwitchToConsensus(bcR.state, bcR.blocksSynced) bcR.eventsFromFSMCh <- bcFsmMessage{event: syncFinishedEv} - } else { - // Should only happen during testing. } + // else { + // Should only happen during testing. + // } } // Implements bcRNotifier diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index d9feef9b4..b81d7d9ac 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -155,12 +155,14 @@ func TestMempoolRmBadTx(t *testing.T) { // and the tx should get removed from the pool err := assertMempool(cs.txNotifier).CheckTx(txBytes, func(r *abci.Response) { if r.GetCheckTx().Code != code.CodeTypeBadNonce { - t.Fatalf("expected checktx to return bad nonce, got %v", r) + t.Errorf("expected checktx to return bad nonce, got %v", r) + return } checkTxRespCh <- struct{}{} }) if err != nil { - t.Fatalf("Error after CheckTx: %v", err) + t.Errorf("Error after CheckTx: %v", err) + return } // check for the tx @@ -180,7 +182,8 @@ func TestMempoolRmBadTx(t *testing.T) { case <-checkTxRespCh: // success case <-ticker: - t.Fatalf("Timed out waiting for tx to return") + t.Errorf("Timed out waiting for tx to return") + return } // Wait until the tx is removed @@ -189,7 +192,8 @@ func TestMempoolRmBadTx(t *testing.T) { case <-emptyMempoolCh: // success case <-ticker: - t.Fatalf("Timed out waiting for tx to be removed") + t.Errorf("Timed out waiting for tx to be removed") + return } } diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index 308c5532f..24b600fb9 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -235,7 +235,7 @@ func TestReactorCreatesBlockWhenEmptyBlocksFalse(t *testing.T) { // send a tx if err := assertMempool(css[3].txNotifier).CheckTx([]byte{1, 2, 3}, nil); err != nil { - //t.Fatal(err) + t.Error(err) } // wait till everyone makes the first new block diff --git a/consensus/state.go b/consensus/state.go index 1f6bad9ab..0a48b0525 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -690,13 +690,13 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { cs.statsMsgQueue <- mi } - if err == ErrAddingVote { - // TODO: punish peer - // We probably don't want to stop the peer here. The vote does not - // necessarily comes from a malicious peer but can be just broadcasted by - // a typical peer. - // https://github.com/tendermint/tendermint/issues/1281 - } + // if err == ErrAddingVote { + // TODO: punish peer + // We probably don't want to stop the peer here. The vote does not + // necessarily comes from a malicious peer but can be just broadcasted by + // a typical peer. + // https://github.com/tendermint/tendermint/issues/1281 + // } // NOTE: the vote is broadcast to peers by the reactor listening // for vote events @@ -709,7 +709,7 @@ func (cs *ConsensusState) handleMsg(mi msgInfo) { return } - if err != nil { + if err != nil { // nolint:staticcheck // Causes TestReactorValidatorSetChanges to timeout // https://github.com/tendermint/tendermint/issues/3406 // cs.Logger.Error("Error with msg", "height", cs.Height, "round", cs.Round, @@ -1227,9 +1227,10 @@ func (cs *ConsensusState) enterCommit(height int64, commitRound int) { cs.ProposalBlockParts = types.NewPartSetFromHeader(blockID.PartsHeader) cs.eventBus.PublishEventValidBlock(cs.RoundStateEvent()) cs.evsw.FireEvent(types.EventValidBlock, &cs.RoundState) - } else { - // We just need to keep waiting. } + // else { + // We just need to keep waiting. + // } } } diff --git a/consensus/state_test.go b/consensus/state_test.go index 93ef0d4cb..1888e4057 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -621,8 +621,6 @@ func TestStateLockPOLUnlock(t *testing.T) { // the proposed block should now be locked and our precommit added validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) - rs = cs1.GetRoundState() - // add precommits from the rest signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs4) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs3) @@ -1317,8 +1315,6 @@ func TestStartNextHeightCorrectly(t *testing.T) { // the proposed block should now be locked and our precommit added validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) - rs = cs1.GetRoundState() - // add precommits signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs3) @@ -1370,8 +1366,6 @@ func TestResetTimeoutPrecommitUponNewHeight(t *testing.T) { ensurePrecommit(voteCh, height, round) validatePrecommit(t, cs1, round, round, vss[0], theBlockHash, theBlockHash) - rs = cs1.GetRoundState() - // add precommits signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2) signAddVotes(cs1, types.PrecommitType, theBlockHash, theBlockParts, vs3) diff --git a/go.sum b/go.sum index ab4763cbe..293135416 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,7 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 h1:Cto4X6SVMWRPBkJ/3YHn1iDGDGc/Z+sW+AEMKHMVvN4= diff --git a/libs/common/async.go b/libs/common/async.go index e3293ab4c..326b97248 100644 --- a/libs/common/async.go +++ b/libs/common/async.go @@ -61,9 +61,10 @@ func (trs *TaskResultSet) Reap() *TaskResultSet { TaskResult: result, OK: true, } - } else { - // We already wrote it. } + // else { + // We already wrote it. + // } default: // Do nothing. } @@ -83,9 +84,10 @@ func (trs *TaskResultSet) Wait() *TaskResultSet { TaskResult: result, OK: true, } - } else { - // We already wrote it. } + // else { + // We already wrote it. + // } } return trs } diff --git a/libs/common/async_test.go b/libs/common/async_test.go index f565b4bd3..c19ffc86f 100644 --- a/libs/common/async_test.go +++ b/libs/common/async_test.go @@ -40,9 +40,10 @@ func TestParallel(t *testing.T) { } else if !assert.Equal(t, -1*i, taskResult.Value.(int)) { assert.Fail(t, "Task should have returned %v but got %v", -1*i, taskResult.Value.(int)) failedTasks++ - } else { - // Good! } + // else { + // Good! + // } } assert.Equal(t, failedTasks, 0, "No task should have failed") assert.Nil(t, trs.FirstError(), "There should be no errors") diff --git a/libs/pubsub/pubsub_test.go b/libs/pubsub/pubsub_test.go index d5f61dc07..5a2baa14f 100644 --- a/libs/pubsub/pubsub_test.go +++ b/libs/pubsub/pubsub_test.go @@ -273,11 +273,11 @@ func TestResubscribe(t *testing.T) { defer s.Stop() ctx := context.Background() - subscription, err := s.Subscribe(ctx, clientID, query.Empty{}) + _, err := s.Subscribe(ctx, clientID, query.Empty{}) require.NoError(t, err) err = s.Unsubscribe(ctx, clientID, query.Empty{}) require.NoError(t, err) - subscription, err = s.Subscribe(ctx, clientID, query.Empty{}) + subscription, err := s.Subscribe(ctx, clientID, query.Empty{}) require.NoError(t, err) err = s.Publish(ctx, "Cable") diff --git a/lite/proxy/query_test.go b/lite/proxy/query_test.go index db2b6e46c..d92a486ea 100644 --- a/lite/proxy/query_test.go +++ b/lite/proxy/query_test.go @@ -143,13 +143,13 @@ func TestTxProofs(t *testing.T) { // First let's make sure a bogus transaction hash returns a valid non-existence proof. key := types.Tx([]byte("bogus")).Hash() - res, err := cl.Tx(key, true) + _, err = cl.Tx(key, true) require.NotNil(err) require.Contains(err.Error(), "not found") // Now let's check with the real tx root hash. key = types.Tx(tx).Hash() - res, err = cl.Tx(key, true) + res, err := cl.Tx(key, true) require.NoError(err, "%#v", err) require.NotNil(res) keyHash := merkle.SimpleHashFromByteSlices([][]byte{key}) diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 81123cb63..fc4591d29 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -250,11 +250,11 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t // so we only record the sender for txs still in the mempool. if e, ok := mem.txsMap.Load(txKey(tx)); ok { memTx := e.(*clist.CElement).Value.(*mempoolTx) - if _, loaded := memTx.senders.LoadOrStore(txInfo.SenderID, true); loaded { - // TODO: consider punishing peer for dups, - // its non-trivial since invalid txs can become valid, - // but they can spam the same tx with little cost to them atm. - } + memTx.senders.LoadOrStore(txInfo.SenderID, true) + // TODO: consider punishing peer for dups, + // its non-trivial since invalid txs can become valid, + // but they can spam the same tx with little cost to them atm. + } return ErrTxInCache diff --git a/p2p/conn/connection_test.go b/p2p/conn/connection_test.go index 283b00ebe..91e3e2099 100644 --- a/p2p/conn/connection_test.go +++ b/p2p/conn/connection_test.go @@ -57,7 +57,8 @@ func TestMConnectionSendFlushStop(t *testing.T) { msgB := make([]byte, aminoMsgLength) _, err := server.Read(msgB) if err != nil { - t.Fatal(err) + t.Error(err) + return } errCh <- err }() diff --git a/p2p/conn/secret_connection_test.go b/p2p/conn/secret_connection_test.go index 76982ed97..9ab9695a3 100644 --- a/p2p/conn/secret_connection_test.go +++ b/p2p/conn/secret_connection_test.go @@ -192,7 +192,8 @@ func writeLots(t *testing.T, wg *sync.WaitGroup, conn net.Conn, txt string, n in for i := 0; i < n; i++ { _, err := conn.Write([]byte(txt)) if err != nil { - t.Fatalf("Failed to write to fooSecConn: %v", err) + t.Errorf("Failed to write to fooSecConn: %v", err) + return } } } @@ -408,7 +409,8 @@ func BenchmarkWriteSecretConnection(b *testing.B) { if err == io.EOF { return } else if err != nil { - b.Fatalf("Failed to read from barSecConn: %v", err) + b.Errorf("Failed to read from barSecConn: %v", err) + return } } }() @@ -418,7 +420,8 @@ func BenchmarkWriteSecretConnection(b *testing.B) { idx := cmn.RandIntn(len(fooWriteBytes)) _, err := fooSecConn.Write(fooWriteBytes[idx]) if err != nil { - b.Fatalf("Failed to write to fooSecConn: %v", err) + b.Errorf("Failed to write to fooSecConn: %v", err) + return } } b.StopTimer() @@ -451,7 +454,8 @@ func BenchmarkReadSecretConnection(b *testing.B) { idx := cmn.RandIntn(len(fooWriteBytes)) _, err := fooSecConn.Write(fooWriteBytes[idx]) if err != nil { - b.Fatalf("Failed to write to fooSecConn: %v, %v,%v", err, i, b.N) + b.Errorf("Failed to write to fooSecConn: %v, %v,%v", err, i, b.N) + return } } }() diff --git a/p2p/switch_test.go b/p2p/switch_test.go index aa5ca78bf..0879acc2d 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -16,7 +16,7 @@ import ( "testing" "time" - stdprometheus "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -348,7 +348,7 @@ func TestSwitchStopsNonPersistentPeerOnError(t *testing.T) { } func TestSwitchStopPeerForError(t *testing.T) { - s := httptest.NewServer(stdprometheus.UninstrumentedHandler()) + s := httptest.NewServer(promhttp.Handler()) defer s.Close() scrapeMetrics := func() string { diff --git a/privval/signer_validator_endpoint_test.go b/privval/signer_validator_endpoint_test.go index bf4c29930..611e743c9 100644 --- a/privval/signer_validator_endpoint_test.go +++ b/privval/signer_validator_endpoint_test.go @@ -331,9 +331,10 @@ func TestErrUnexpectedResponse(t *testing.T) { // we do not want to Start() the remote signer here and instead use the connection to // reply with intentionally wrong replies below: rsConn, err := serviceEndpoint.connect() - defer rsConn.Close() require.NoError(t, err) require.NotNil(t, rsConn) + defer rsConn.Close() + // send over public key to get the remote signer running: go testReadWriteResponse(t, &PubKeyResponse{}, rsConn) <-readyCh diff --git a/rpc/grpc/client_server.go b/rpc/grpc/client_server.go index 922016dd5..d02120e10 100644 --- a/rpc/grpc/client_server.go +++ b/rpc/grpc/client_server.go @@ -2,8 +2,8 @@ package core_grpc import ( "net" - "time" + "golang.org/x/net/context" "google.golang.org/grpc" cmn "github.com/tendermint/tendermint/libs/common" @@ -26,13 +26,13 @@ func StartGRPCServer(ln net.Listener) error { // StartGRPCClient dials the gRPC server using protoAddr and returns a new // BroadcastAPIClient. func StartGRPCClient(protoAddr string) BroadcastAPIClient { - conn, err := grpc.Dial(protoAddr, grpc.WithInsecure(), grpc.WithDialer(dialerFunc)) + conn, err := grpc.Dial(protoAddr, grpc.WithInsecure(), grpc.WithContextDialer(dialerFunc)) if err != nil { panic(err) } return NewBroadcastAPIClient(conn) } -func dialerFunc(addr string, timeout time.Duration) (net.Conn, error) { +func dialerFunc(ctx context.Context, addr string) (net.Conn, error) { return cmn.Connect(addr) } diff --git a/rpc/lib/client/ws_client.go b/rpc/lib/client/ws_client.go index e3b559569..05180c753 100644 --- a/rpc/lib/client/ws_client.go +++ b/rpc/lib/client/ws_client.go @@ -369,10 +369,11 @@ func (c *WSClient) writeRoutine() { defer func() { ticker.Stop() - if err := c.conn.Close(); err != nil { - // ignore error; it will trigger in tests - // likely because it's closing an already closed connection - } + c.conn.Close() + // err != nil { + // ignore error; it will trigger in tests + // likely because it's closing an already closed connection + // } c.wg.Done() }() @@ -421,10 +422,11 @@ func (c *WSClient) writeRoutine() { // executing all reads from this goroutine. func (c *WSClient) readRoutine() { defer func() { - if err := c.conn.Close(); err != nil { - // ignore error; it will trigger in tests - // likely because it's closing an already closed connection - } + c.conn.Close() + // err != nil { + // ignore error; it will trigger in tests + // likely because it's closing an already closed connection + // } c.wg.Done() }() diff --git a/rpc/lib/client/ws_client_test.go b/rpc/lib/client/ws_client_test.go index e902fe21a..4f2cc9ada 100644 --- a/rpc/lib/client/ws_client_test.go +++ b/rpc/lib/client/ws_client_test.go @@ -212,7 +212,8 @@ func callWgDoneOnResult(t *testing.T, c *WSClient, wg *sync.WaitGroup) { select { case resp := <-c.ResponsesCh: if resp.Error != nil { - t.Fatalf("unexpected error: %v", resp.Error) + t.Errorf("unexpected error: %v", resp.Error) + return } if resp.Result != nil { wg.Done() diff --git a/state/state_test.go b/state/state_test.go index a0f7a4a2a..2604a93b8 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -185,11 +185,11 @@ func TestValidatorSimpleSaveLoad(t *testing.T) { assert := assert.New(t) // Can't load anything for height 0. - v, err := sm.LoadValidators(stateDB, 0) + _, err := sm.LoadValidators(stateDB, 0) assert.IsType(sm.ErrNoValSetForHeight{}, err, "expected err at height 0") // Should be able to load for height 1. - v, err = sm.LoadValidators(stateDB, 1) + v, err := sm.LoadValidators(stateDB, 1) assert.Nil(err, "expected no err at height 1") assert.Equal(v.Hash(), state.Validators.Hash(), "expected validator hashes to match") diff --git a/tools/tm-monitor/mock/eventmeter.go b/tools/tm-monitor/mock/eventmeter.go index 7bbedc7fa..7119c4399 100644 --- a/tools/tm-monitor/mock/eventmeter.go +++ b/tools/tm-monitor/mock/eventmeter.go @@ -54,7 +54,7 @@ func (c *RpcClient) Call(method string, params map[string]interface{}, result in } rv, rt := reflect.ValueOf(result), reflect.TypeOf(result) - rv, rt = rv.Elem(), rt.Elem() + rv, _ = rv.Elem(), rt.Elem() rv.Set(reflect.ValueOf(s)) return s, nil diff --git a/types/genesis_test.go b/types/genesis_test.go index f977513e7..33bdd34c1 100644 --- a/types/genesis_test.go +++ b/types/genesis_test.go @@ -68,7 +68,7 @@ func TestGenesisGood(t *testing.T) { genDoc.ConsensusParams.Block.MaxBytes = 0 genDocBytes, err = cdc.MarshalJSON(genDoc) assert.NoError(t, err, "error marshalling genDoc") - genDoc, err = GenesisDocFromJSON(genDocBytes) + _, err = GenesisDocFromJSON(genDocBytes) assert.Error(t, err, "expected error for genDoc json with block size of 0") // Genesis doc from raw json diff --git a/types/validator_set.go b/types/validator_set.go index 65358714d..2078e7a95 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -619,10 +619,11 @@ func (vals *ValidatorSet) VerifyCommit(chainID string, blockID BlockID, height i // Good precommit! if blockID.Equals(precommit.BlockID) { talliedVotingPower += val.VotingPower - } else { - // It's OK that the BlockID doesn't match. We include stray - // precommits to measure validator availability. } + // else { + // It's OK that the BlockID doesn't match. We include stray + // precommits to measure validator availability. + // } } if talliedVotingPower > vals.TotalVotingPower()*2/3 { @@ -703,10 +704,11 @@ func (vals *ValidatorSet) VerifyFutureCommit(newSet *ValidatorSet, chainID strin // Good precommit! if blockID.Equals(precommit.BlockID) { oldVotingPower += val.VotingPower - } else { - // It's OK that the BlockID doesn't match. We include stray - // precommits to measure validator availability. } + // else { + // It's OK that the BlockID doesn't match. We include stray + // precommits to measure validator availability. + // } } if oldVotingPower <= oldVals.TotalVotingPower()*2/3 { From ef1b3d8ebbcf99ea2cf251083d25243be4d98762 Mon Sep 17 00:00:00 2001 From: Marko Date: Thu, 25 Jul 2019 10:13:19 +0200 Subject: [PATCH 153/211] types: move MakeVote / MakeBlock functions (#3819) to the types package Paritally Fixes #3584 --- blockchain/v0/reactor_test.go | 26 ++++++----------------- consensus/replay_test.go | 24 +++------------------ state/helpers_test.go | 20 +----------------- state/validation_test.go | 4 ++-- types/block.go | 19 ----------------- types/test_util.go | 40 +++++++++++++++++++++++++++++++++-- 6 files changed, 50 insertions(+), 83 deletions(-) diff --git a/blockchain/v0/reactor_test.go b/blockchain/v0/reactor_test.go index 2000b134d..7aa17ec68 100644 --- a/blockchain/v0/reactor_test.go +++ b/blockchain/v0/reactor_test.go @@ -45,24 +45,6 @@ func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.G }, privValidators } -func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { - addr := privVal.GetPubKey().Address() - idx, _ := valset.GetByAddress(addr) - vote := &types.Vote{ - ValidatorAddress: addr, - ValidatorIndex: idx, - Height: header.Height, - Round: 1, - Timestamp: tmtime.Now(), - Type: types.PrecommitType, - BlockID: blockID, - } - - privVal.SignVote(header.ChainID, vote) - - return vote -} - type BlockchainReactorPair struct { reactor *BlockchainReactor app proxy.AppConns @@ -106,8 +88,12 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) lastBlock := blockStore.LoadBlock(blockHeight - 1) - vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() - lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + vote, err := types.MakeVote(lastBlock.Header.Height, lastBlockMeta.BlockID, state.Validators, privVals[0], lastBlock.Header.ChainID) + if err != nil { + panic(err) + } + voteCommitSig := vote.CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{voteCommitSig}) } thisBlock := makeBlock(blockHeight, state, lastCommit) diff --git a/consensus/replay_test.go b/consensus/replay_test.go index bbb5b6678..0d2226875 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -29,7 +29,6 @@ import ( "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" "github.com/tendermint/tendermint/version" ) @@ -849,31 +848,14 @@ func makeBlocks(n int, state *sm.State, privVal types.PrivValidator) []*types.Bl return blocks } -func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { - addr := privVal.GetPubKey().Address() - idx, _ := valset.GetByAddress(addr) - vote := &types.Vote{ - ValidatorAddress: addr, - ValidatorIndex: idx, - Height: header.Height, - Round: 1, - Timestamp: tmtime.Now(), - Type: types.PrecommitType, - BlockID: blockID, - } - - privVal.SignVote(header.ChainID, vote) - - return vote -} - func makeBlock(state sm.State, lastBlock *types.Block, lastBlockMeta *types.BlockMeta, privVal types.PrivValidator, height int64) (*types.Block, *types.PartSet) { lastCommit := types.NewCommit(types.BlockID{}, nil) if height > 1 { - vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVal).CommitSig() - lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) + vote, _ := types.MakeVote(lastBlock.Header.Height, lastBlockMeta.BlockID, state.Validators, privVal, lastBlock.Header.ChainID) + voteCommitSig := vote.CommitSig() + lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{voteCommitSig}) } return state.MakeBlock(height, []types.Tx{}, lastCommit, nil, state.Validators.GetProposer().Address) diff --git a/state/helpers_test.go b/state/helpers_test.go index e8cb27585..c1ef13582 100644 --- a/state/helpers_test.go +++ b/state/helpers_test.go @@ -69,29 +69,11 @@ func makeAndApplyGoodBlock(state sm.State, height int64, lastCommit *types.Commi return state, blockID, nil } -func makeVote(height int64, blockID types.BlockID, valSet *types.ValidatorSet, privVal types.PrivValidator) (*types.Vote, error) { - addr := privVal.GetPubKey().Address() - idx, _ := valSet.GetByAddress(addr) - vote := &types.Vote{ - ValidatorAddress: addr, - ValidatorIndex: idx, - Height: height, - Round: 0, - Timestamp: tmtime.Now(), - Type: types.PrecommitType, - BlockID: blockID, - } - if err := privVal.SignVote(chainID, vote); err != nil { - return nil, err - } - return vote, nil -} - func makeValidCommit(height int64, blockID types.BlockID, vals *types.ValidatorSet, privVals map[string]types.PrivValidator) (*types.Commit, error) { sigs := make([]*types.CommitSig, 0) for i := 0; i < vals.Size(); i++ { _, val := vals.GetByIndex(i) - vote, err := makeVote(height, blockID, vals, privVals[val.Address.String()]) + vote, err := types.MakeVote(height, blockID, vals, privVals[val.Address.String()], chainID) if err != nil { return nil, err } diff --git a/state/validation_test.go b/state/validation_test.go index c53cf0102..c0dd6e569 100644 --- a/state/validation_test.go +++ b/state/validation_test.go @@ -101,7 +101,7 @@ func TestValidateBlockCommit(t *testing.T) { #2589: ensure state.LastValidators.VerifyCommit fails here */ // should be height-1 instead of height - wrongHeightVote, err := makeVote(height, state.LastBlockID, state.Validators, privVals[proposerAddr.String()]) + wrongHeightVote, err := types.MakeVote(height, state.LastBlockID, state.Validators, privVals[proposerAddr.String()], chainID) require.NoError(t, err, "height %d", height) wrongHeightCommit := types.NewCommit(state.LastBlockID, []*types.CommitSig{wrongHeightVote.CommitSig()}) block, _ := state.MakeBlock(height, makeTxs(height), wrongHeightCommit, nil, proposerAddr) @@ -129,7 +129,7 @@ func TestValidateBlockCommit(t *testing.T) { /* wrongPrecommitsCommit is fine except for the extra bad precommit */ - goodVote, err := makeVote(height, blockID, state.Validators, privVals[proposerAddr.String()]) + goodVote, err := types.MakeVote(height, blockID, state.Validators, privVals[proposerAddr.String()], chainID) require.NoError(t, err, "height %d", height) badVote := &types.Vote{ ValidatorAddress: badPrivVal.GetPubKey().Address(), diff --git a/types/block.go b/types/block.go index 55709ad60..5dc0ff6a7 100644 --- a/types/block.go +++ b/types/block.go @@ -41,25 +41,6 @@ type Block struct { LastCommit *Commit `json:"last_commit"` } -// MakeBlock returns a new block with an empty header, except what can be -// computed from itself. -// It populates the same set of fields validated by ValidateBasic. -func MakeBlock(height int64, txs []Tx, lastCommit *Commit, evidence []Evidence) *Block { - block := &Block{ - Header: Header{ - Height: height, - NumTxs: int64(len(txs)), - }, - Data: Data{ - Txs: txs, - }, - Evidence: EvidenceData{Evidence: evidence}, - LastCommit: lastCommit, - } - block.fillHeader() - return block -} - // ValidateBasic performs basic validation that doesn't involve state data. // It checks the internal consistency of the block. // Further validation is done using state#ValidateBlock. diff --git a/types/test_util.go b/types/test_util.go index 18e472148..d226fd99e 100644 --- a/types/test_util.go +++ b/types/test_util.go @@ -5,8 +5,7 @@ import ( ) func MakeCommit(blockID BlockID, height int64, round int, - voteSet *VoteSet, - validators []PrivValidator) (*Commit, error) { + voteSet *VoteSet, validators []PrivValidator) (*Commit, error) { // all sign for i := 0; i < len(validators); i++ { @@ -37,3 +36,40 @@ func signAddVote(privVal PrivValidator, vote *Vote, voteSet *VoteSet) (signed bo } return voteSet.AddVote(vote) } + +func MakeVote(height int64, blockID BlockID, valSet *ValidatorSet, privVal PrivValidator, chainID string) (*Vote, error) { + addr := privVal.GetPubKey().Address() + idx, _ := valSet.GetByAddress(addr) + vote := &Vote{ + ValidatorAddress: addr, + ValidatorIndex: idx, + Height: height, + Round: 0, + Timestamp: tmtime.Now(), + Type: PrecommitType, + BlockID: blockID, + } + if err := privVal.SignVote(chainID, vote); err != nil { + return nil, err + } + return vote, nil +} + +// MakeBlock returns a new block with an empty header, except what can be +// computed from itself. +// It populates the same set of fields validated by ValidateBasic. +func MakeBlock(height int64, txs []Tx, lastCommit *Commit, evidence []Evidence) *Block { + block := &Block{ + Header: Header{ + Height: height, + NumTxs: int64(len(txs)), + }, + Data: Data{ + Txs: txs, + }, + Evidence: EvidenceData{Evidence: evidence}, + LastCommit: lastCommit, + } + block.fillHeader() + return block +} From 44b913bf2f4a7d84a625efb320d3f72ba3fdfddb Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 25 Jul 2019 15:06:18 +0400 Subject: [PATCH 154/211] p2p: Fix error logging for connection stop (#3824) * p2p: fix false-positive error logging when stopping connections This changeset fixes two types of false-positive errors occurring during connection shutdown. The first occurs when the process invokes FlushStop() or Stop() on a connection. While the previous behavior did properly wait for the sendRoutine to finish, it did not notify the recvRoutine that the connection was shutting down. This would cause the recvRouting to receive and error when reading and log this error. The changeset fixes this by notifying the recvRoutine that the connection is shutting down. The second occurs when the connection is terminated (gracefully) by the other side. The recvRoutine would get an EOF error during the read, log it, and stop the connection with an error. The changeset detects EOF and gracefully shuts down the connection. * bring back the comment about flushing * add changelog entry * listen for quitRecvRoutine too * we have to call stopForError Otherwise peer won't be removed from the peer set and maybe readded later. --- p2p/conn/connection.go | 32 ++++++++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/p2p/conn/connection.go b/p2p/conn/connection.go index ee29fc85c..a206af542 100644 --- a/p2p/conn/connection.go +++ b/p2p/conn/connection.go @@ -90,6 +90,9 @@ type MConnection struct { quitSendRoutine chan struct{} doneSendRoutine chan struct{} + // Closing quitRecvRouting will cause the recvRouting to eventually quit. + quitRecvRoutine chan struct{} + // used to ensure FlushStop and OnStop // are safe to call concurrently. stopMtx sync.Mutex @@ -206,6 +209,7 @@ func (c *MConnection) OnStart() error { c.chStatsTimer = time.NewTicker(updateStats) c.quitSendRoutine = make(chan struct{}) c.doneSendRoutine = make(chan struct{}) + c.quitRecvRoutine = make(chan struct{}) go c.sendRoutine() go c.recvRoutine() return nil @@ -220,7 +224,14 @@ func (c *MConnection) stopServices() (alreadyStopped bool) { select { case <-c.quitSendRoutine: - // already quit via FlushStop or OnStop + // already quit + return true + default: + } + + select { + case <-c.quitRecvRoutine: + // already quit return true default: } @@ -230,6 +241,8 @@ func (c *MConnection) stopServices() (alreadyStopped bool) { c.pingTimer.Stop() c.chStatsTimer.Stop() + // inform the recvRouting that we are shutting down + close(c.quitRecvRoutine) close(c.quitSendRoutine) return false } @@ -250,8 +263,6 @@ func (c *MConnection) FlushStop() { <-c.doneSendRoutine // Send and flush all pending msgs. - // By now, IsRunning == false, - // so any concurrent attempts to send will fail. // Since sendRoutine has exited, we can call this // safely eof := c.sendSomePacketMsgs() @@ -550,9 +561,22 @@ FOR_LOOP: var err error _n, err = cdc.UnmarshalBinaryLengthPrefixedReader(c.bufConnReader, &packet, int64(c._maxPacketMsgSize)) c.recvMonitor.Update(int(_n)) + if err != nil { + // stopServices was invoked and we are shutting down + // receiving is excpected to fail since we will close the connection + select { + case <-c.quitRecvRoutine: + break FOR_LOOP + default: + } + if c.IsRunning() { - c.Logger.Error("Connection failed @ recvRoutine (reading byte)", "conn", c, "err", err) + if err == io.EOF { + c.Logger.Info("Connection is closed @ recvRoutine (likely by the other side)", "conn", c) + } else { + c.Logger.Error("Connection failed @ recvRoutine (reading byte)", "conn", c, "err", err) + } c.stopForError(err) } break FOR_LOOP From 06c0545e829f2071d4b7f601076c38b56cdddecc Mon Sep 17 00:00:00 2001 From: folex <0xdxdy@gmail.com> Date: Thu, 25 Jul 2019 14:58:14 +0300 Subject: [PATCH 155/211] p2p: Do not write 'Couldn't connect to any seeds' if there are no seeds (#3834) * Do not write 'Couldn't connect to any seeds' if there are no seeds * changelog * remove privValUpgrade * Fix typo in changelog * Update CHANGELOG_PENDING.md Co-Authored-By: Marko I'm setting up all peers dynamically by calling dial_peers, so p2p.seeds in configs is empty, and I'm seeing error log a lot in logs. --- p2p/pex/pex_reactor.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/p2p/pex/pex_reactor.go b/p2p/pex/pex_reactor.go index 557e7ca75..55cde5a35 100644 --- a/p2p/pex/pex_reactor.go +++ b/p2p/pex/pex_reactor.go @@ -594,7 +594,10 @@ func (r *PEXReactor) dialSeeds() { } r.Switch.Logger.Error("Error dialing seed", "err", err, "seed", seedAddr) } - r.Switch.Logger.Error("Couldn't connect to any seeds") + // do not write error message if there were no seeds specified in config + if len(r.seedAddrs) > 0 { + r.Switch.Logger.Error("Couldn't connect to any seeds") + } } // AttemptsToDial returns the number of attempts to dial specific address. It From 0811aafdc2d0a9bfb79f55d3495195ea5aa02c0f Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Thu, 25 Jul 2019 20:39:39 +0400 Subject: [PATCH 156/211] docs: add a footer to guides (#3835) --- docs/guides/go-built-in.md | 13 +++++++++++-- docs/guides/go.md | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/docs/guides/go-built-in.md b/docs/guides/go-built-in.md index a0c76c9e9..705022c90 100644 --- a/docs/guides/go-built-in.md +++ b/docs/guides/go-built-in.md @@ -1,4 +1,6 @@ -# 1 Guide Assumptions +# Creating a built-in application in Go + +## Guide assumptions This guide is designed for beginners who want to get started with a Tendermint Core application from scratch. It does not assume that you have any prior @@ -17,7 +19,7 @@ yourself with the syntax. By following along with this guide, you'll create a Tendermint Core project called kvstore, a (very) simple distributed BFT key-value store. -# 1 Creating a built-in application in Go +## Built-in app vs external app Running your application inside the same process as Tendermint Core will give you the best possible performance. @@ -628,3 +630,10 @@ $ curl -s 'localhost:26657/abci_query?data="tendermint"' "dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of "tendermint" and "rocks" accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). diff --git a/docs/guides/go.md b/docs/guides/go.md index abda07955..ada84adfc 100644 --- a/docs/guides/go.md +++ b/docs/guides/go.md @@ -1,4 +1,6 @@ -# 1 Guide Assumptions +# Creating an application in Go + +## Guide Assumptions This guide is designed for beginners who want to get started with a Tendermint Core application from scratch. It does not assume that you have any prior @@ -17,7 +19,7 @@ yourself with the syntax. By following along with this guide, you'll create a Tendermint Core project called kvstore, a (very) simple distributed BFT key-value store. -# 1 Creating an application in Go +## Built-in app vs external app To get maximum performance it is better to run your application alongside Tendermint Core. [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written @@ -512,3 +514,10 @@ $ curl -s 'localhost:26657/abci_query?data="tendermint"' "dGVuZGVybWludA==" and "cm9ja3M=" are the base64-encoding of the ASCII of "tendermint" and "rocks" accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). From a3f45208ac26ca9213d864d95be81ba4458b4e6a Mon Sep 17 00:00:00 2001 From: Ivan Kushmantsev Date: Mon, 29 Jul 2019 21:48:11 +0400 Subject: [PATCH 157/211] docs: "Writing a Tendermint Core application in Kotlin (gRPC)" guide (#3838) * add abci grpc kotlin guide * Update docs/guides/kotlin.md Co-Authored-By: Anton Kaliaev * Update docs/guides/kotlin.md Co-Authored-By: Anton Kaliaev * Update docs/guides/kotlin.md Co-Authored-By: Anton Kaliaev * Update kotlin.md --- docs/guides/kotlin.md | 575 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 575 insertions(+) create mode 100644 docs/guides/kotlin.md diff --git a/docs/guides/kotlin.md b/docs/guides/kotlin.md new file mode 100644 index 000000000..8f462bd61 --- /dev/null +++ b/docs/guides/kotlin.md @@ -0,0 +1,575 @@ +# Creating an application in Kotlin + +## Guide Assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine (your application) - written in any programming language - and securely +replicates it on many machines. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. The application (which should +implementing the blockchain interface (ABCI)) will be written in Kotlin. + +This guide assumes that you are not new to JVM world. If you are new please see [JVM Minimal Survival Guide](https://hadihariri.com/2013/12/29/jvm-minimal-survival-guide-for-the-dotnet-developer/#java-the-language-java-the-ecosystem-java-the-jvm) and [Gradle Docs](https://docs.gradle.org/current/userguide/userguide.html). + +## Built-in app vs external app + +If you use Golang, you can run your app and Tendermint Core in the same process to get maximum performance. +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written this way. +Please refer to [Writing a built-in Tendermint Core application in Go](./go-built-in.md) guide for details. + +If you choose another language, like we did in this guide, you have to write a separate app using +either plain socket or gRPC. This guide will show you how to build external applicationg +using RPC server. + +Having a separate application might give you better security guarantees as two +processes would be communicating via established binary protocol. Tendermint +Core will not have access to application's state. + +## 1.1 Installing Java and Gradle + +Please refer to [the Oracle's guide for installing JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html). + +Verify that you have installed Java successully: + +```sh +$ java -version +java version "1.8.0_162" +Java(TM) SE Runtime Environment (build 1.8.0_162-b12) +Java HotSpot(TM) 64-Bit Server VM (build 25.162-b12, mixed mode) +``` + +You can choose any version of Java higher or equal to 8. +In my case it is Java SE Development Kit 8. + +Make sure you have `$JAVA_HOME` environment variable set: + +```sh +$ echo $JAVA_HOME +/Library/Java/JavaVirtualMachines/jdk1.8.0_162.jdk/Contents/Home +``` + +For Gradle installation, please refer to [their official guide](https://gradle.org/install/). + +## 1.2 Creating a new Kotlin project + +We'll start by creating a new Gradle project. + +```sh +$ export KVSTORE_HOME=~/kvstore +$ mkdir $KVSTORE_HOME +$ cd $KVSTORE_HOME +``` + +Inside the example directory run: +```sh +gradle init --dsl groovy --package io.example --project-name example --type kotlin-application +``` +That Gradle command will create project structure for you: +```sh +$ tree +. +|-- build.gradle +|-- gradle +| `-- wrapper +| |-- gradle-wrapper.jar +| `-- gradle-wrapper.properties +|-- gradlew +|-- gradlew.bat +|-- settings.gradle +`-- src + |-- main + | |-- kotlin + | | `-- io + | | `-- example + | | `-- App.kt + | `-- resources + `-- test + |-- kotlin + | `-- io + | `-- example + | `-- AppTest.kt + `-- resources +``` + +When run, this should print "Hello world." to the standard output. + +```sh +$ ./gradlew run +> Task :run +Hello world. +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +### 1.3.1 Compile .proto files + +Add folowing to the top of `build.gradle`: +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8' + } +} +``` + +Enable protobuf plugin in `plugins` section of `build.gradle`: +```groovy +plugins { + id 'com.google.protobuf' version '0.8.8' +} +``` + +Add following to `build.gradle`: +```groovy +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.7.1" + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.22.1' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} +``` + +Now your project is ready to compile `*.proto` files. + + +Copy necessary .proto files to your project: +```sh +mkdir -p \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto + +cp $GOPATH/src/github.com/tendermint/tendermint/abci/types/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types/types.proto +cp $GOPATH/src/github.com/tendermint/tendermint/crypto/merkle/merkle.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle/merkle.proto +cp $GOPATH/src/github.com/tendermint/tendermint/libs/common/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common/types.proto +cp $GOPATH/src/github.com/gogo/protobuf/gogoproto/gogo.proto \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto/gogo.proto +``` + +Add dependency to `build.gradle`: +```groovy +dependencies { + implementation 'io.grpc:grpc-protobuf:1.22.1' + implementation 'io.grpc:grpc-netty-shaded:1.22.1' + implementation 'io.grpc:grpc-stub:1.22.1' +} +``` + +To generate all protobuf-type classes run: +```sh +./gradlew generateProto +``` +It will produce java classes to `build/generated/`: +```sh +$ tree build/generated/ +build/generated/ +`-- source + `-- proto + `-- main + |-- grpc + | `-- types + | `-- ABCIApplicationGrpc.java + `-- java + |-- com + | `-- google + | `-- protobuf + | `-- GoGoProtos.java + |-- common + | `-- Types.java + |-- merkle + | `-- Merkle.java + `-- types + `-- Types.java +``` + +### 1.3.2 Implementing ABCI + +As you can see there is a generated file `$KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java`. +which contains an abstract class `ABCIApplicationImplBase`. This class fully describes the ABCI interface. +All you need is implement this interface. + +Create file `$KVSTORE_HOME/src/main/kotlin/io/example/KVStoreApp.kt` with following context: +```kotlin +package io.example + +import io.grpc.stub.StreamObserver +import types.ABCIApplicationGrpc +import types.Types.* + +class KVStoreApp : ABCIApplicationGrpc.ABCIApplicationImplBase() { + + // methods implementation + +} +``` + +Now I will go through each method of `ABCIApplicationImplBase` explaining when it's called and adding +required business logic. + +### 1.3.3 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```kotlin +override fun checkTx(req: RequestCheckTx, responseObserver: StreamObserver) { + val code = req.tx.validate() + val resp = ResponseCheckTx.newBuilder() + .setCode(code) + .setGasWanted(1) + .build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} + +private fun ByteString.validate(): Int { + val parts = this.split('=') + if (parts.size != 2) { + return 1 + } + val key = parts[0] + val value = parts[1] + + // check if the same key=value already exists + val stored = getPersistedValue(key) + if (stored != null && stored.contentEquals(value)) { + return 2 + } + + return 0 +} + +private fun ByteString.split(separator: Char): List { + val arr = this.toByteArray() + val i = (0 until this.size()).firstOrNull { arr[it] == separator.toByte() } + ?: return emptyList() + return listOf( + this.substring(0, i).toByteArray(), + this.substring(i + 1).toByteArray() + ) +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[JetBrains Xodus](https://github.com/JetBrains/xodus), which is a transactional schema-less embedded high-performance database written in Java. + +`build.gradle`: +```groovy +dependencies { + implementation "org.jetbrains.xodus:xodus-environment:1.3.91" +} +``` + +```kotlin +... +import jetbrains.exodus.ArrayByteIterable +import jetbrains.exodus.env.Environment +import jetbrains.exodus.env.Store +import jetbrains.exodus.env.StoreConfig +import jetbrains.exodus.env.Transaction + +class KVStoreApp( + private val env: Environment +) : ABCIApplicationGrpc.ABCIApplicationImplBase() { + + private var txn: Transaction? = null + private var store: Store? = null + + ... +} +``` + +### 1.3.4 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transfered to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. DeliverTx are being transfered asynchronously, but the +responses are expected to come in order. + +```kotlin +override fun beginBlock(req: RequestBeginBlock, responseObserver: StreamObserver) { + txn = env.beginTransaction() + store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn!!) + val resp = ResponseBeginBlock.newBuilder().build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} +``` +Here we start new transaction, which will store block's transactions, and open corresponding store. + +```kotlin +override fun deliverTx(req: RequestDeliverTx, responseObserver: StreamObserver) { + val code = req.tx.validate() + if (code == 0) { + val parts = req.tx.split('=') + val key = ArrayByteIterable(parts[0]) + val value = ArrayByteIterable(parts[1]) + store!!.put(txn!!, key, value) + } + val resp = ResponseDeliverTx.newBuilder() + .setCode(code) + .build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the storage. + +In the current design, a block can include incorrect transactions (those who +passed CheckTx, but failed DeliverTx or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```kotlin +override fun commit(req: RequestCommit, responseObserver: StreamObserver) { + txn!!.commit() + val resp = ResponseCommit.newBuilder() + .setData(ByteString.copyFrom(ByteArray(8))) + .build() + responseObserver.onNext(resp) + responseObserver.onCompleted() +} +``` + +### 1.3.5 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```kotlin +override fun query(req: RequestQuery, responseObserver: StreamObserver) { + val k = req.data.toByteArray() + val v = getPersistedValue(k) + val builder = ResponseQuery.newBuilder() + if (v == null) { + builder.log = "does not exist" + } else { + builder.log = "exists" + builder.key = ByteString.copyFrom(k) + builder.value = ByteString.copyFrom(v) + } + responseObserver.onNext(builder.build()) + responseObserver.onCompleted() +} + +private fun getPersistedValue(k: ByteArray): ByteArray? { + return env.computeInReadonlyTransaction { txn -> + val store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn) + store.get(txn, ArrayByteIterable(k))?.bytesUnsafe + } +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instances + +Put the following code into the `$KVSTORE_HOME/src/main/kotlin/io/example/App.kt` file: + +```kotlin +package io.example + +import jetbrains.exodus.env.Environments + +fun main() { + Environments.newInstance("tmp/storage").use { env -> + val app = KVStoreApp(env) + val server = GrpcServer(app, 26658) + server.start() + server.blockUntilShutdown() + } +} +``` + +It is the entry point of the application. +Here we create special object `Environment` which knows where to store state of the application. +Then we create and srart gRPC server to handle Tendermint's requests. + +Create file `$KVSTORE_HOME/src/main/kotlin/io/example/GrpcServer.kt`: +```kotlin +package io.example + +import io.grpc.BindableService +import io.grpc.ServerBuilder + +class GrpcServer( + private val service: BindableService, + private val port: Int +) { + private val server = ServerBuilder + .forPort(port) + .addService(service) + .build() + + fun start() { + server.start() + println("gRPC server started, listening on $port") + Runtime.getRuntime().addShutdownHook(object : Thread() { + override fun run() { + println("shutting down gRPC server since JVM is shutting down") + this@GrpcServer.stop() + println("server shut down") + } + }) + } + + fun stop() { + server.shutdown() + } + + /** + * Await termination on the main thread since the grpc library uses daemon threads. + */ + fun blockUntilShutdown() { + server.awaitTermination() + } + +} +``` + +## 1.5 Getting Up and Running + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +Feel free to explore the generated files, which can be found at +`/tmp/example/config` directory. Documentation on the config can be found +[here](https://tendermint.com/docs/tendermint-core/configuration.html). + +We are ready to start our application: + +```sh +./gradlew run + +gRPC server started, listening on 26658 +``` + +Then we need to start Tendermint Core and point it to our application. Staying +within the application directory execute: + +```sh +$ TMHOME="/tmp/example" tendermint node --abci grpc --proxy_app tcp://127.0.0.1:26658 + +I[2019-07-28|15:44:53.632] Version info module=main software=0.32.1 block=10 p2p=7 +I[2019-07-28|15:44:53.677] Starting Node module=main impl=Node +I[2019-07-28|15:44:53.681] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:7639e2841ccd47d5ae0f5aad3011b14049d3f452 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-Nhl3zk Version:0.32.1 Channels:4020212223303800 Moniker:Ivans-MacBook-Pro.local Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}" +I[2019-07-28|15:44:54.801] Executed block module=state height=8 validTxs=0 invalidTxs=0 +I[2019-07-28|15:44:54.814] Committed state module=state height=8 txs=0 appHash=0000000000000000 +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB", + "height": "33" +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +```sh +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3My" + } + } +} +``` + +`dGVuZGVybWludA==` and `cm9ja3M=` are the base64-encoding of the ASCII of `tendermint` and `rocks` accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). + +The full source code of this example project can be found [here](https://github.com/climber73/tendermint-abci-grpc-kotlin). From 261abe349fc65e83ff4038860f640eac9a42c994 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 30 Jul 2019 17:08:11 +0400 Subject: [PATCH 158/211] node: allow replacing existing p2p.Reactor(s) (#3846) * node: allow replacing existing p2p.Reactor(s) using [`CustomReactors` option](https://godoc.org/github.com/tendermint/tendermint/node#CustomReactors). Warning: beware of accidental name clashes. Here is the list of existing reactors: MEMPOOL, BLOCKCHAIN, CONSENSUS, EVIDENCE, PEX. * check the absence of "CUSTOM" prefix * merge 2 tests * add doc.go to node package --- CHANGELOG_PENDING.md | 4 ++++ node/doc.go | 40 ++++++++++++++++++++++++++++++++++++++++ node/node.go | 23 +++++++++++++++++------ node/node_test.go | 7 ++++++- p2p/switch.go | 23 +++++++++++++++++++---- 5 files changed, 86 insertions(+), 11 deletions(-) create mode 100644 node/doc.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index fd5c4b27e..678f98b93 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -16,6 +16,10 @@ program](https://hackerone.com/tendermint). - Go API ### FEATURES: +- [node] Allow replacing existing p2p.Reactor(s) using [`CustomReactors` + option](https://godoc.org/github.com/tendermint/tendermint/node#CustomReactors). + Warning: beware of accidental name clashes. Here is the list of existing + reactors: MEMPOOL, BLOCKCHAIN, CONSENSUS, EVIDENCE, PEX. ### IMPROVEMENTS: diff --git a/node/doc.go b/node/doc.go new file mode 100644 index 000000000..08f3fa258 --- /dev/null +++ b/node/doc.go @@ -0,0 +1,40 @@ +/* +Package node is the main entry point, where the Node struct, which +represents a full node, is defined. + +Adding new p2p.Reactor(s) + +To add a new p2p.Reactor, use the CustomReactors option: + + node, err := NewNode( + config, + privVal, + nodeKey, + clientCreator, + genesisDocProvider, + dbProvider, + metricsProvider, + logger, + CustomReactors(map[string]p2p.Reactor{"CUSTOM": customReactor}), + ) + +Replacing existing p2p.Reactor(s) + +To replace the built-in p2p.Reactor, use the CustomReactors option: + + node, err := NewNode( + config, + privVal, + nodeKey, + clientCreator, + genesisDocProvider, + dbProvider, + metricsProvider, + logger, + CustomReactors(map[string]p2p.Reactor{"BLOCKCHAIN": customBlockchainReactor}), + ) + +The list of existing reactors can be found in CustomReactors documentation. + +*/ +package node diff --git a/node/node.go b/node/node.go index 9ccf57691..8497f3a63 100644 --- a/node/node.go +++ b/node/node.go @@ -48,10 +48,6 @@ import ( "github.com/tendermint/tendermint/version" ) -// CustomReactorNamePrefix is a prefix for all custom reactors to prevent -// clashes with built-in reactors. -const CustomReactorNamePrefix = "CUSTOM_" - //------------------------------------------------------------------------------ // DBContext specifies config information for loading a new DB. @@ -144,11 +140,26 @@ func DefaultMetricsProvider(config *cfg.InstrumentationConfig) MetricsProvider { // Option sets a parameter for the node. type Option func(*Node) -// CustomReactors allows you to add custom reactors to the node's Switch. +// CustomReactors allows you to add custom reactors (name -> p2p.Reactor) to +// the node's Switch. +// +// WARNING: using any name from the below list of the existing reactors will +// result in replacing it with the custom one. +// +// - MEMPOOL +// - BLOCKCHAIN +// - CONSENSUS +// - EVIDENCE +// - PEX func CustomReactors(reactors map[string]p2p.Reactor) Option { return func(n *Node) { for name, reactor := range reactors { - n.sw.AddReactor(CustomReactorNamePrefix+name, reactor) + if existingReactor := n.sw.Reactor(name); existingReactor != nil { + n.sw.Logger.Info("Replacing existing reactor with a custom one", + "name", name, "existing", existingReactor, "custom", reactor) + n.sw.RemoveReactor(name, existingReactor) + } + n.sw.AddReactor(name, reactor) } } } diff --git a/node/node_test.go b/node/node_test.go index 841a04686..0ce61d35e 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -288,6 +288,7 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { defer os.RemoveAll(config.RootDir) cr := p2pmock.NewReactor() + customBlockchainReactor := p2pmock.NewReactor() nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) require.NoError(t, err) @@ -300,7 +301,7 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { DefaultDBProvider, DefaultMetricsProvider(config.Instrumentation), log.TestingLogger(), - CustomReactors(map[string]p2p.Reactor{"FOO": cr}), + CustomReactors(map[string]p2p.Reactor{"FOO": cr, "BLOCKCHAIN": customBlockchainReactor}), ) require.NoError(t, err) @@ -309,6 +310,10 @@ func TestNodeNewNodeCustomReactors(t *testing.T) { defer n.Stop() assert.True(t, cr.IsRunning()) + assert.Equal(t, cr, n.Switch().Reactor("FOO")) + + assert.True(t, customBlockchainReactor.IsRunning()) + assert.Equal(t, customBlockchainReactor, n.Switch().Reactor("BLOCKCHAIN")) } func state(nVals int, height int64) (sm.State, dbm.DB) { diff --git a/p2p/switch.go b/p2p/switch.go index 7e681d67c..66c2f9e4a 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -152,11 +152,9 @@ func WithMetrics(metrics *Metrics) SwitchOption { // AddReactor adds the given reactor to the switch. // NOTE: Not goroutine safe. func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { - // Validate the reactor. - // No two reactors can share the same channel. - reactorChannels := reactor.GetChannels() - for _, chDesc := range reactorChannels { + for _, chDesc := range reactor.GetChannels() { chID := chDesc.ID + // No two reactors can share the same channel. if sw.reactorsByCh[chID] != nil { panic(fmt.Sprintf("Channel %X has multiple reactors %v & %v", chID, sw.reactorsByCh[chID], reactor)) } @@ -168,6 +166,23 @@ func (sw *Switch) AddReactor(name string, reactor Reactor) Reactor { return reactor } +// RemoveReactor removes the given Reactor from the Switch. +// NOTE: Not goroutine safe. +func (sw *Switch) RemoveReactor(name string, reactor Reactor) { + for _, chDesc := range reactor.GetChannels() { + // remove channel description + for i := 0; i < len(sw.chDescs); i++ { + if chDesc.ID == sw.chDescs[i].ID { + sw.chDescs = append(sw.chDescs[:i], sw.chDescs[i+1:]...) + break + } + } + delete(sw.reactorsByCh, chDesc.ID) + } + delete(sw.reactors, name) + reactor.SetSwitch(nil) +} + // Reactors returns a map of reactors registered on the switch. // NOTE: Not goroutine safe. func (sw *Switch) Reactors() map[string]Reactor { From ec2c1fd735d0168dfcbdb3de59c96e942579af27 Mon Sep 17 00:00:00 2001 From: Marko Date: Tue, 30 Jul 2019 16:13:35 +0200 Subject: [PATCH 159/211] gocritic (1/2) (#3836) Add gocritic as a linter The linting is not complete, but should i complete in this PR or in a following. 23 files have been touched so it may be better to do in a following PR Commits: * Add gocritic to linting - Added gocritic to linting Signed-off-by: Marko Baricevic * gocritic * pr comments * remove switch in cmdBatch --- abci/cmd/abci-cli/abci-cli.go | 50 +++++--------------- abci/example/kvstore/persistent_kvstore.go | 3 +- abci/server/socket_server.go | 7 +-- cmd/tendermint/commands/root_test.go | 2 +- consensus/mempool_test.go | 4 +- consensus/reactor_test.go | 12 ++--- consensus/replay.go | 6 +-- consensus/replay_file.go | 6 +-- consensus/state.go | 6 +-- consensus/state_test.go | 36 +++++++------- crypto/ed25519/ed25519.go | 4 +- crypto/internal/benchmarking/bench.go | 4 +- crypto/secp256k1/internal/secp256k1/curve.go | 1 + libs/cli/helper.go | 2 +- libs/common/random_test.go | 8 ++-- libs/common/string.go | 7 +-- mempool/reactor_test.go | 8 ++-- p2p/node_info_test.go | 2 +- p2p/pex/addrbook.go | 6 +-- rpc/lib/server/handlers.go | 10 ++-- state/state_test.go | 18 +++---- types/genesis.go | 6 +-- types/validator_set.go | 4 +- 23 files changed, 89 insertions(+), 123 deletions(-) diff --git a/abci/cmd/abci-cli/abci-cli.go b/abci/cmd/abci-cli/abci-cli.go index cd0a6fd1f..5f0685107 100644 --- a/abci/cmd/abci-cli/abci-cli.go +++ b/abci/cmd/abci-cli/abci-cli.go @@ -174,9 +174,7 @@ where example.file looks something like: info `, Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdBatch(cmd, args) - }, + RunE: cmdBatch, } var consoleCmd = &cobra.Command{ @@ -189,9 +187,7 @@ without opening a new connection each time `, Args: cobra.ExactArgs(0), ValidArgs: []string{"echo", "info", "set_option", "deliver_tx", "check_tx", "commit", "query"}, - RunE: func(cmd *cobra.Command, args []string) error { - return cmdConsole(cmd, args) - }, + RunE: cmdConsole, } var echoCmd = &cobra.Command{ @@ -199,27 +195,21 @@ var echoCmd = &cobra.Command{ Short: "have the application echo a message", Long: "have the application echo a message", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdEcho(cmd, args) - }, + RunE: cmdEcho, } var infoCmd = &cobra.Command{ Use: "info", Short: "get some info about the application", Long: "get some info about the application", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdInfo(cmd, args) - }, + RunE: cmdInfo, } var setOptionCmd = &cobra.Command{ Use: "set_option", Short: "set an option on the application", Long: "set an option on the application", Args: cobra.ExactArgs(2), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdSetOption(cmd, args) - }, + RunE: cmdSetOption, } var deliverTxCmd = &cobra.Command{ @@ -227,9 +217,7 @@ var deliverTxCmd = &cobra.Command{ Short: "deliver a new transaction to the application", Long: "deliver a new transaction to the application", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdDeliverTx(cmd, args) - }, + RunE: cmdDeliverTx, } var checkTxCmd = &cobra.Command{ @@ -237,9 +225,7 @@ var checkTxCmd = &cobra.Command{ Short: "validate a transaction", Long: "validate a transaction", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdCheckTx(cmd, args) - }, + RunE: cmdCheckTx, } var commitCmd = &cobra.Command{ @@ -247,9 +233,7 @@ var commitCmd = &cobra.Command{ Short: "commit the application state and return the Merkle root hash", Long: "commit the application state and return the Merkle root hash", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdCommit(cmd, args) - }, + RunE: cmdCommit, } var versionCmd = &cobra.Command{ @@ -268,9 +252,7 @@ var queryCmd = &cobra.Command{ Short: "query the application state", Long: "query the application state", Args: cobra.ExactArgs(1), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdQuery(cmd, args) - }, + RunE: cmdQuery, } var counterCmd = &cobra.Command{ @@ -278,9 +260,7 @@ var counterCmd = &cobra.Command{ Short: "ABCI demo example", Long: "ABCI demo example", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdCounter(cmd, args) - }, + RunE: cmdCounter, } var kvstoreCmd = &cobra.Command{ @@ -288,9 +268,7 @@ var kvstoreCmd = &cobra.Command{ Short: "ABCI demo example", Long: "ABCI demo example", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdKVStore(cmd, args) - }, + RunE: cmdKVStore, } var testCmd = &cobra.Command{ @@ -298,9 +276,7 @@ var testCmd = &cobra.Command{ Short: "run integration tests", Long: "run integration tests", Args: cobra.ExactArgs(0), - RunE: func(cmd *cobra.Command, args []string) error { - return cmdTest(cmd, args) - }, + RunE: cmdTest, } // Generates new Args array based off of previous call args to maintain flag persistence @@ -419,7 +395,7 @@ func muxOnCommands(cmd *cobra.Command, pArgs []string) error { } // otherwise, we need to skip the next one too - i += 1 + i++ continue } diff --git a/abci/example/kvstore/persistent_kvstore.go b/abci/example/kvstore/persistent_kvstore.go index f4276b7cd..a0f336af6 100644 --- a/abci/example/kvstore/persistent_kvstore.go +++ b/abci/example/kvstore/persistent_kvstore.go @@ -121,8 +121,7 @@ func (app *PersistentKVStoreApplication) BeginBlock(req types.RequestBeginBlock) app.ValUpdates = make([]types.ValidatorUpdate, 0) for _, ev := range req.ByzantineValidators { - switch ev.Type { - case tmtypes.ABCIEvidenceTypeDuplicateVote: + if ev.Type == tmtypes.ABCIEvidenceTypeDuplicateVote { // decrease voting power by 1 if ev.TotalVotingPower == 0 { continue diff --git a/abci/server/socket_server.go b/abci/server/socket_server.go index 82ce610ed..3e1d775d7 100644 --- a/abci/server/socket_server.go +++ b/abci/server/socket_server.go @@ -127,11 +127,12 @@ func (s *SocketServer) acceptConnectionsRoutine() { func (s *SocketServer) waitForClose(closeConn chan error, connID int) { err := <-closeConn - if err == io.EOF { + switch { + case err == io.EOF: s.Logger.Error("Connection was closed by client") - } else if err != nil { + case err != nil: s.Logger.Error("Connection error", "error", err) - } else { + default: // never happens s.Logger.Error("Connection was closed.") } diff --git a/cmd/tendermint/commands/root_test.go b/cmd/tendermint/commands/root_test.go index 892a49b74..229385af9 100644 --- a/cmd/tendermint/commands/root_test.go +++ b/cmd/tendermint/commands/root_test.go @@ -165,7 +165,7 @@ func TestRootConfig(t *testing.T) { func WriteConfigVals(dir string, vals map[string]string) error { data := "" for k, v := range vals { - data = data + fmt.Sprintf("%s = \"%s\"\n", k, v) + data += fmt.Sprintf("%s = \"%s\"\n", k, v) } cfile := filepath.Join(dir, "config.toml") return ioutil.WriteFile(cfile, []byte(data), 0666) diff --git a/consensus/mempool_test.go b/consensus/mempool_test.go index b81d7d9ac..afd8f7194 100644 --- a/consensus/mempool_test.go +++ b/consensus/mempool_test.go @@ -82,14 +82,14 @@ func TestMempoolProgressInHigherRound(t *testing.T) { ensureNewRound(newRoundCh, height, round) // first round at first height ensureNewEventOnChannel(newBlockCh) // first block gets committed - height = height + 1 // moving to the next height + height++ // moving to the next height round = 0 ensureNewRound(newRoundCh, height, round) // first round at next height deliverTxsRange(cs, 0, 1) // we deliver txs, but dont set a proposal so we get the next round ensureNewTimeout(timeoutCh, height, round, cs.config.TimeoutPropose.Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) // wait for the next round ensureNewEventOnChannel(newBlockCh) // now we can commit the block } diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index 24b600fb9..86c63859a 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -31,15 +31,15 @@ import ( //---------------------------------------------- // in-process testnets -func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ( +func startConsensusNet(t *testing.T, css []*ConsensusState, n int) ( []*ConsensusReactor, []types.Subscription, []*types.EventBus, ) { - reactors := make([]*ConsensusReactor, N) + reactors := make([]*ConsensusReactor, n) blocksSubs := make([]types.Subscription, 0) - eventBuses := make([]*types.EventBus, N) - for i := 0; i < N; i++ { + eventBuses := make([]*types.EventBus, n) + for i := 0; i < n; i++ { /*logger, err := tmflags.ParseLogLevel("consensus:info,*:error", logger, "info") if err != nil { t.Fatal(err)}*/ reactors[i] = NewConsensusReactor(css[i], true) // so we dont start the consensus states @@ -58,7 +58,7 @@ func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ( } } // make connected switches and start all reactors - p2p.MakeConnectedSwitches(config.P2P, N, func(i int, s *p2p.Switch) *p2p.Switch { + p2p.MakeConnectedSwitches(config.P2P, n, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("CONSENSUS", reactors[i]) s.SetLogger(reactors[i].conS.Logger.With("module", "p2p")) return s @@ -68,7 +68,7 @@ func startConsensusNet(t *testing.T, css []*ConsensusState, N int) ( // If we started the state machines before everyone was connected, // we'd block when the cs fires NewBlockEvent and the peers are trying to start their reactors // TODO: is this still true with new pubsub? - for i := 0; i < N; i++ { + for i := 0; i < n; i++ { s := reactors[i].conS.GetState() reactors[i].SwitchToConsensus(s, 0) } diff --git a/consensus/replay.go b/consensus/replay.go index 2c4377ffa..52e46d854 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -320,11 +320,9 @@ func (h *Handshaker) ReplayBlocks( } state.Validators = types.NewValidatorSet(vals) state.NextValidators = types.NewValidatorSet(vals) - } else { + } else if len(h.genDoc.Validators) == 0 { // If validator set is not set in genesis and still empty after InitChain, exit. - if len(h.genDoc.Validators) == 0 { - return nil, fmt.Errorf("validator set is nil in genesis and still empty after InitChain") - } + return nil, fmt.Errorf("validator set is nil in genesis and still empty after InitChain") } if res.ConsensusParams != nil { diff --git a/consensus/replay_file.go b/consensus/replay_file.go index 215627a5d..b8b84e721 100644 --- a/consensus/replay_file.go +++ b/consensus/replay_file.go @@ -231,10 +231,8 @@ func (pb *playback) replayConsoleLoop() int { 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 { - pb.cs.Logger.Error("Replay reset error", "err", err) - } + } else if err := pb.replayReset(i, newStepSub); err != nil { + pb.cs.Logger.Error("Replay reset error", "err", err) } } diff --git a/consensus/state.go b/consensus/state.go index 0a48b0525..fe6fefd42 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -924,10 +924,8 @@ func (cs *ConsensusState) defaultDecideProposal(height int64, round int) { } cs.Logger.Info("Signed proposal", "height", height, "round", round, "proposal", proposal) cs.Logger.Debug(fmt.Sprintf("Signed proposal block: %v", block)) - } else { - if !cs.replayMode { - cs.Logger.Error("enterPropose: Error signing proposal", "height", height, "round", round, "err", err) - } + } else if !cs.replayMode { + cs.Logger.Error("enterPropose: Error signing proposal", "height", height, "round", round, "err", err) } } diff --git a/consensus/state_test.go b/consensus/state_test.go index 1888e4057..8409f2235 100644 --- a/consensus/state_test.go +++ b/consensus/state_test.go @@ -181,7 +181,7 @@ func TestStateBadProposal(t *testing.T) { propBlock, _ := cs1.createProposalBlock() //changeProposer(t, cs1, vs2) // make the second validator the proposer by incrementing round - round = round + 1 + round++ incrementRound(vss[1:]...) // make the block bad by tampering with statehash @@ -374,7 +374,7 @@ func TestStateLockNoPOL(t *testing.T) { /// - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 1") /* @@ -418,7 +418,7 @@ func TestStateLockNoPOL(t *testing.T) { // then we enterPrecommitWait and timeout into NewRound ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // entering new round + round++ // entering new round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 2") /* @@ -460,7 +460,7 @@ func TestStateLockNoPOL(t *testing.T) { incrementRound(vs2) - round = round + 1 // entering new round + round++ // entering new round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 3") /* @@ -544,7 +544,7 @@ func TestStateLockPOLRelock(t *testing.T) { // timeout to new round ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round //XXX: this isnt guaranteed to get there before the timeoutPropose ... if err := cs1.SetProposalAndBlock(prop, propBlock, propBlockParts, "some peer"); err != nil { t.Fatal(err) @@ -635,7 +635,7 @@ func TestStateLockPOLUnlock(t *testing.T) { lockedBlockHash := rs.LockedBlock.Hash() incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) t.Log("#### ONTO ROUND 1") @@ -718,7 +718,7 @@ func TestStateLockPOLSafety1(t *testing.T) { incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) //XXX: this isnt guaranteed to get there before the timeoutPropose ... @@ -755,7 +755,7 @@ func TestStateLockPOLSafety1(t *testing.T) { ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -821,7 +821,7 @@ func TestStateLockPOLSafety2(t *testing.T) { incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round t.Log("### ONTO Round 1") // jump in at round 1 startTestRound(cs1, height, round) @@ -850,7 +850,7 @@ func TestStateLockPOLSafety2(t *testing.T) { // timeout of precommit wait to new round ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round // in round 2 we see the polkad block from round 0 newProp := types.NewProposal(height, round, 0, propBlockID0) if err := vs3.SignProposal(config.ChainID(), newProp); err != nil { @@ -920,7 +920,7 @@ func TestProposeValidBlock(t *testing.T) { ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) incrementRound(vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -945,14 +945,14 @@ func TestProposeValidBlock(t *testing.T) { signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - round = round + 2 // moving to the next round + round += 2 // moving to the next round ensureNewRound(newRoundCh, height, round) t.Log("### ONTO ROUND 3") ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) @@ -1044,7 +1044,7 @@ func TestSetValidBlockOnDelayedProposal(t *testing.T) { voteCh := subscribeToVoter(cs1, addr) proposalCh := subscribe(cs1.eventBus, types.EventQueryCompleteProposal) - round = round + 1 // move to round in which P0 is not proposer + round++ // move to round in which P0 is not proposer incrementRound(vs2, vs3, vs4) startTestRound(cs1, cs1.Height, round) @@ -1123,7 +1123,7 @@ func TestWaitingTimeoutProposeOnNewRound(t *testing.T) { incrementRound(vss[1:]...) signAddVotes(cs1, types.PrevoteType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) rs := cs1.GetRoundState() @@ -1157,7 +1157,7 @@ func TestRoundSkipOnNilPolkaFromHigherRound(t *testing.T) { incrementRound(vss[1:]...) signAddVotes(cs1, types.PrecommitType, nil, types.PartSetHeader{}, vs2, vs3, vs4) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) ensurePrecommit(voteCh, height, round) @@ -1165,7 +1165,7 @@ func TestRoundSkipOnNilPolkaFromHigherRound(t *testing.T) { ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) } @@ -1511,7 +1511,7 @@ func TestStateHalt1(t *testing.T) { // timeout to new round ensureNewTimeout(timeoutWaitCh, height, round, cs1.config.Precommit(round).Nanoseconds()) - round = round + 1 // moving to the next round + round++ // moving to the next round ensureNewRound(newRoundCh, height, round) rs = cs1.GetRoundState() diff --git a/crypto/ed25519/ed25519.go b/crypto/ed25519/ed25519.go index bc60838d5..8947608ae 100644 --- a/crypto/ed25519/ed25519.go +++ b/crypto/ed25519/ed25519.go @@ -54,7 +54,7 @@ func (privKey PrivKeyEd25519) Bytes() []byte { // incorrect signature. func (privKey PrivKeyEd25519) Sign(msg []byte) ([]byte, error) { signatureBytes := ed25519.Sign(privKey[:], msg) - return signatureBytes[:], nil + return signatureBytes, nil } // PubKey gets the corresponding public key from the private key. @@ -100,7 +100,7 @@ func GenPrivKey() PrivKeyEd25519 { // genPrivKey generates a new ed25519 private key using the provided reader. func genPrivKey(rand io.Reader) PrivKeyEd25519 { seed := make([]byte, 32) - _, err := io.ReadFull(rand, seed[:]) + _, err := io.ReadFull(rand, seed) if err != nil { panic(err) } diff --git a/crypto/internal/benchmarking/bench.go b/crypto/internal/benchmarking/bench.go index c988de48e..43ab312f0 100644 --- a/crypto/internal/benchmarking/bench.go +++ b/crypto/internal/benchmarking/bench.go @@ -24,10 +24,10 @@ func (zeroReader) Read(buf []byte) (int, error) { // BenchmarkKeyGeneration benchmarks the given key generation algorithm using // a dummy reader. -func BenchmarkKeyGeneration(b *testing.B, GenerateKey func(reader io.Reader) crypto.PrivKey) { +func BenchmarkKeyGeneration(b *testing.B, generateKey func(reader io.Reader) crypto.PrivKey) { var zero zeroReader for i := 0; i < b.N; i++ { - GenerateKey(zero) + generateKey(zero) } } diff --git a/crypto/secp256k1/internal/secp256k1/curve.go b/crypto/secp256k1/internal/secp256k1/curve.go index 5409ee1d2..df87200f2 100644 --- a/crypto/secp256k1/internal/secp256k1/curve.go +++ b/crypto/secp256k1/internal/secp256k1/curve.go @@ -30,6 +30,7 @@ // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// nolint:gocritic package secp256k1 import ( diff --git a/libs/cli/helper.go b/libs/cli/helper.go index 878cf26e5..6bf23750c 100644 --- a/libs/cli/helper.go +++ b/libs/cli/helper.go @@ -14,7 +14,7 @@ import ( func WriteConfigVals(dir string, vals map[string]string) error { data := "" for k, v := range vals { - data = data + fmt.Sprintf("%s = \"%s\"\n", k, v) + data += fmt.Sprintf("%s = \"%s\"\n", k, v) } cfile := filepath.Join(dir, "config.toml") return ioutil.WriteFile(cfile, []byte(data), 0666) diff --git a/libs/common/random_test.go b/libs/common/random_test.go index c59a577b8..74dcc04b4 100644 --- a/libs/common/random_test.go +++ b/libs/common/random_test.go @@ -45,11 +45,9 @@ func TestDeterminism(t *testing.T) { output := testThemAll() if i == 0 { firstOutput = output - } else { - if firstOutput != output { - t.Errorf("Run #%d's output was different from first run.\nfirst: %v\nlast: %v", - i, firstOutput, output) - } + } else if firstOutput != output { + t.Errorf("Run #%d's output was different from first run.\nfirst: %v\nlast: %v", + i, firstOutput, output) } } } diff --git a/libs/common/string.go b/libs/common/string.go index ddf350b10..4f8a8f20d 100644 --- a/libs/common/string.go +++ b/libs/common/string.go @@ -51,11 +51,12 @@ func IsASCIIText(s string) bool { func ASCIITrim(s string) string { r := make([]byte, 0, len(s)) for _, b := range []byte(s) { - if b == 32 { + switch { + case b == 32: continue // skip space - } else if 32 < b && b <= 126 { + case 32 < b && b <= 126: r = append(r, b) - } else { + default: panic(fmt.Sprintf("non-ASCII (non-tab) char 0x%X", b)) } } diff --git a/mempool/reactor_test.go b/mempool/reactor_test.go index 94c0d1900..dff4c0d68 100644 --- a/mempool/reactor_test.go +++ b/mempool/reactor_test.go @@ -42,10 +42,10 @@ func mempoolLogger() log.Logger { } // connect N mempool reactors through N switches -func makeAndConnectReactors(config *cfg.Config, N int) []*Reactor { - reactors := make([]*Reactor, N) +func makeAndConnectReactors(config *cfg.Config, n int) []*Reactor { + reactors := make([]*Reactor, n) logger := mempoolLogger() - for i := 0; i < N; i++ { + for i := 0; i < n; i++ { app := kvstore.NewKVStoreApplication() cc := proxy.NewLocalClientCreator(app) mempool, cleanup := newMempoolWithApp(cc) @@ -55,7 +55,7 @@ func makeAndConnectReactors(config *cfg.Config, N int) []*Reactor { reactors[i].SetLogger(logger.With("validator", i)) } - p2p.MakeConnectedSwitches(config.P2P, N, func(i int, s *p2p.Switch) *p2p.Switch { + p2p.MakeConnectedSwitches(config.P2P, n, func(i int, s *p2p.Switch) *p2p.Switch { s.AddReactor("MEMPOOL", reactors[i]) return s diff --git a/p2p/node_info_test.go b/p2p/node_info_test.go index 19567d2bf..9ed80b28b 100644 --- a/p2p/node_info_test.go +++ b/p2p/node_info_test.go @@ -19,7 +19,7 @@ func TestNodeInfoValidate(t *testing.T) { channels[i] = byte(i) } dupChannels := make([]byte, 5) - copy(dupChannels[:], channels[:5]) + copy(dupChannels, channels[:5]) dupChannels = append(dupChannels, testCh) nonAscii := "¢§µ" diff --git a/p2p/pex/addrbook.go b/p2p/pex/addrbook.go index cfe2569ba..27bcef9e8 100644 --- a/p2p/pex/addrbook.go +++ b/p2p/pex/addrbook.go @@ -178,11 +178,11 @@ func (a *addrBook) OurAddress(addr *p2p.NetAddress) bool { return ok } -func (a *addrBook) AddPrivateIDs(IDs []string) { +func (a *addrBook) AddPrivateIDs(ids []string) { a.mtx.Lock() defer a.mtx.Unlock() - for _, id := range IDs { + for _, id := range ids { a.privateIDs[p2p.ID(id)] = struct{}{} } } @@ -643,7 +643,7 @@ func (a *addrBook) randomPickAddresses(bucketType byte, num int) []*p2p.NetAddre } total := 0 for _, bucket := range buckets { - total = total + len(bucket) + total += len(bucket) } addresses := make([]*knownAddress, 0, total) for _, bucket := range buckets { diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index 434ee8916..78bc7b259 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -735,12 +735,10 @@ func (wsc *wsConnection) writeRoutine() { jsonBytes, err := json.MarshalIndent(msg, "", " ") if err != nil { wsc.Logger.Error("Failed to marshal RPCResponse to JSON", "err", err) - } else { - if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { - wsc.Logger.Error("Failed to write response", "err", err) - wsc.Stop() - return - } + } else if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { + wsc.Logger.Error("Failed to write response", "err", err) + wsc.Stop() + return } case <-wsc.Quit(): return diff --git a/state/state_test.go b/state/state_test.go index 2604a93b8..fceda3cc7 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -440,13 +440,13 @@ func TestProposerPriorityDoesNotGetResetToZero(t *testing.T) { // 3. Center - with avg, resulting val2:-61, val1:62 avg := big.NewInt(0).Add(big.NewInt(wantVal1Prio), big.NewInt(wantVal2Prio)) avg.Div(avg, big.NewInt(2)) - wantVal2Prio = wantVal2Prio - avg.Int64() // -61 - wantVal1Prio = wantVal1Prio - avg.Int64() // 62 + wantVal2Prio -= avg.Int64() // -61 + wantVal1Prio -= avg.Int64() // 62 // 4. Steps from IncrementProposerPriority - wantVal1Prio = wantVal1Prio + val1VotingPower // 72 - wantVal2Prio = wantVal2Prio + val2VotingPower // 39 - wantVal1Prio = wantVal1Prio - totalPowerAfter // -38 as val1 is proposer + wantVal1Prio += val1VotingPower // 72 + wantVal2Prio += val2VotingPower // 39 + wantVal1Prio -= totalPowerAfter // -38 as val1 is proposer assert.Equal(t, wantVal1Prio, updatedVal1.ProposerPriority) assert.Equal(t, wantVal2Prio, addedVal2.ProposerPriority) @@ -563,9 +563,9 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { expectedVal2Prio := v2PrioWhenAddedVal2 - avg.Int64() // -11 expectedVal1Prio := oldVal1.ProposerPriority - avg.Int64() // 11 // 4. Increment - expectedVal2Prio = expectedVal2Prio + val2VotingPower // -11 + 10 = -1 - expectedVal1Prio = expectedVal1Prio + val1VotingPower // 11 + 10 == 21 - expectedVal1Prio = expectedVal1Prio - totalPower // 1, val1 proposer + expectedVal2Prio += val2VotingPower // -11 + 10 = -1 + expectedVal1Prio += val1VotingPower // 11 + 10 == 21 + expectedVal1Prio -= totalPower // 1, val1 proposer assert.EqualValues(t, expectedVal1Prio, updatedVal1.ProposerPriority) assert.EqualValues(t, expectedVal2Prio, updatedVal2.ProposerPriority, "unexpected proposer priority for validator: %v", updatedVal2) @@ -589,7 +589,7 @@ func TestProposerPriorityProposerAlternates(t *testing.T) { // Increment expectedVal2Prio2 := expectedVal2Prio + val2VotingPower // -1 + 10 = 9 expectedVal1Prio2 := expectedVal1Prio + val1VotingPower // 1 + 10 == 11 - expectedVal1Prio2 = expectedVal1Prio2 - totalPower // -9, val1 proposer + expectedVal1Prio2 -= totalPower // -9, val1 proposer assert.EqualValues(t, expectedVal1Prio2, updatedVal1.ProposerPriority, "unexpected proposer priority for validator: %v", updatedVal2) assert.EqualValues(t, expectedVal2Prio2, updatedVal2.ProposerPriority, "unexpected proposer priority for validator: %v", updatedVal2) diff --git a/types/genesis.go b/types/genesis.go index 54b81e9e2..de59fc87e 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -72,10 +72,8 @@ func (genDoc *GenesisDoc) ValidateAndComplete() error { if genDoc.ConsensusParams == nil { genDoc.ConsensusParams = DefaultConsensusParams() - } else { - if err := genDoc.ConsensusParams.Validate(); err != nil { - return err - } + } else if err := genDoc.ConsensusParams.Validate(); err != nil { + return err } for i, v := range genDoc.Validators { diff --git a/types/validator_set.go b/types/validator_set.go index 2078e7a95..33636d092 100644 --- a/types/validator_set.go +++ b/types/validator_set.go @@ -121,7 +121,7 @@ func (vals *ValidatorSet) RescalePriorities(diffMax int64) { ratio := (diff + diffMax - 1) / diffMax if diff > diffMax { for _, val := range vals.Validators { - val.ProposerPriority = val.ProposerPriority / ratio + val.ProposerPriority /= ratio } } } @@ -525,7 +525,7 @@ func (vals *ValidatorSet) applyRemovals(deletes []*Validator) { // The 'allowDeletes' flag is set to false by NewValidatorSet() and to true by UpdateWithChangeSet(). func (vals *ValidatorSet) updateWithChangeSet(changes []*Validator, allowDeletes bool) error { - if len(changes) <= 0 { + if len(changes) == 0 { return nil } From 22bbc41cb461c90a15b5e94e52eb86aaf4a74ff2 Mon Sep 17 00:00:00 2001 From: Marko Date: Wed, 31 Jul 2019 16:17:09 +0200 Subject: [PATCH 160/211] version tmdb (#3854) --- go.sum | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/go.sum b/go.sum index 293135416..0e1ee3358 100644 --- a/go.sum +++ b/go.sum @@ -123,6 +123,13 @@ github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 h1:1oFLiOyVl+W7 github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= +<<<<<<< HEAD +======= +github.com/tendermint/tm-db v0.0.0-20190731085305-94017c88bf1d h1:yCHL2COLGLNfb4sA9AlzIHpapb8UATvAQyJulS6Eg6Q= +github.com/tendermint/tm-db v0.0.0-20190731085305-94017c88bf1d/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= +github.com/tendermint/tm-db v0.1.1 h1:G3Xezy3sOk9+ekhjZ/kjArYIs1SmwV+1OUgNkj7RgV0= +github.com/tendermint/tm-db v0.1.1/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= +>>>>>>> d56fb6ed... version tmdb (#3854) go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= From 602c046f1961185f435bde06e5410c3654a4b3fe Mon Sep 17 00:00:00 2001 From: Alexander Bezobchuk Date: Wed, 31 Jul 2019 08:01:55 -0700 Subject: [PATCH 161/211] txindexer: Refactor Tx Search Aggregation (#3851) - Replace the previous intersect call, which was called at each query condition, with a map intersection. - Replace fmt.Sprintf with string() closes: #3076 Benchmarks ``` Old goos: darwin goarch: amd64 pkg: github.com/tendermint/tendermint/state/txindex/kv BenchmarkTxSearch-4 200 103641206 ns/op 7998416 B/op 71171 allocs/op PASS ok github.com/tendermint/tendermint/state/txindex/kv 26.019s New goos: darwin goarch: amd64 pkg: github.com/tendermint/tendermint/state/txindex/kv BenchmarkTxSearch-4 1000 38615024 ns/op 13515226 B/op 166460 allocs/op PASS ok github.com/tendermint/tendermint/state/txindex/kv 53.618s ``` ~62% performance improvement Commits: * Refactor tx search * Add pending changelog entry * Add tx search benchmarking * remove intermediate hashes list also reset timer in BenchmarkTxSearch and fix other benchmark * fix import * Add test cases * Fix searching * Replace fmt.Sprintf with string * Update state/txindex/kv/kv.go Co-Authored-By: Anton Kaliaev * Rename params * Cleanup * Check error in benchmarks --- CHANGELOG_PENDING.md | 12 +++ state/txindex/kv/kv.go | 141 +++++++++++++++++++++--------- state/txindex/kv/kv_bench_test.go | 72 +++++++++++++++ state/txindex/kv/kv_test.go | 9 +- 4 files changed, 194 insertions(+), 40 deletions(-) create mode 100644 state/txindex/kv/kv_bench_test.go diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 678f98b93..6488ffa90 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -23,4 +23,16 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: +<<<<<<< HEAD +======= +- [p2p] \#3834 Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file +- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) +- [rpc] \#2252 Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence +- [rpc] \#3818 Make `max_body_bytes` and `max_header_bytes` configurable +- [p2p] \#3664 p2p/conn: reuse buffer when write/read from secret connection +- [mempool] \#3826 Make `max_msg_bytes` configurable +- [blockchain] \#3561 Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) +- [rpc] \#3076 Improve transaction search performance + +>>>>>>> aacc71dc... txindexer: Refactor Tx Search Aggregation (#3851) ### BUG FIXES: diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index 053d26a71..803eb9199 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -11,6 +11,7 @@ import ( "github.com/pkg/errors" + cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/pubsub/query" @@ -163,8 +164,8 @@ func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.S // both lower and upper bounds, so we are not performing a full scan. Results // from querying indexes are then intersected and returned to the caller. func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { - var hashes [][]byte var hashesInitialized bool + filteredHashes := make(map[string][]byte) // get a list of conditions (like "tx.height > 5") conditions := q.Conditions() @@ -193,10 +194,16 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { for _, r := range ranges { if !hashesInitialized { - hashes = txi.matchRange(r, startKey(r.key)) + filteredHashes = txi.matchRange(r, startKey(r.key), filteredHashes, true) hashesInitialized = true + + // Ignore any remaining conditions if the first condition resulted + // in no matches (assuming implicit AND operand). + if len(filteredHashes) == 0 { + break + } } else { - hashes = intersect(hashes, txi.matchRange(r, startKey(r.key))) + filteredHashes = txi.matchRange(r, startKey(r.key), filteredHashes, false) } } } @@ -211,21 +218,26 @@ func (txi *TxIndex) Search(q *query.Query) ([]*types.TxResult, error) { } if !hashesInitialized { - hashes = txi.match(c, startKeyForCondition(c, height)) + filteredHashes = txi.match(c, startKeyForCondition(c, height), filteredHashes, true) hashesInitialized = true + + // Ignore any remaining conditions if the first condition resulted + // in no matches (assuming implicit AND operand). + if len(filteredHashes) == 0 { + break + } } else { - hashes = intersect(hashes, txi.match(c, startKeyForCondition(c, height))) + filteredHashes = txi.match(c, startKeyForCondition(c, height), filteredHashes, false) } } - results := make([]*types.TxResult, len(hashes)) - i := 0 - for _, h := range hashes { - results[i], err = txi.Get(h) + results := make([]*types.TxResult, 0, len(filteredHashes)) + for _, h := range filteredHashes { + res, err := txi.Get(h) if err != nil { return nil, errors.Wrapf(err, "failed to get Tx{%X}", h) } - i++ + results = append(results, res) } // sort by height & index by default @@ -353,63 +365,115 @@ func isRangeOperation(op query.Operator) bool { } } -func (txi *TxIndex) match(c query.Condition, startKeyBz []byte) (hashes [][]byte) { +// match returns all matching txs by hash that meet a given condition and start +// key. An already filtered result (filteredHashes) is provided such that any +// non-intersecting matches are removed. +// +// NOTE: filteredHashes may be empty if no previous condition has matched. +func (txi *TxIndex) match(c query.Condition, startKeyBz []byte, filteredHashes map[string][]byte, firstRun bool) map[string][]byte { + // A previous match was attempted but resulted in no matches, so we return + // no matches (assuming AND operand). + if !firstRun && len(filteredHashes) == 0 { + return filteredHashes + } + + tmpHashes := make(map[string][]byte) + if c.Op == query.OpEqual { it := dbm.IteratePrefix(txi.store, startKeyBz) defer it.Close() + for ; it.Valid(); it.Next() { - hashes = append(hashes, it.Value()) + tmpHashes[string(it.Value())] = it.Value() } + } else if c.Op == query.OpContains { // 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 := dbm.IteratePrefix(txi.store, startKey(c.Tag)) defer it.Close() + for ; it.Valid(); it.Next() { if !isTagKey(it.Key()) { continue } + if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) { - hashes = append(hashes, it.Value()) + tmpHashes[string(it.Value())] = it.Value() } } } else { panic("other operators should be handled already") } - return + + if len(tmpHashes) == 0 || firstRun { + // Either: + // + // 1. Regardless if a previous match was attempted, which may have had + // results, but no match was found for the current condition, then we + // return no matches (assuming AND operand). + // + // 2. A previous match was not attempted, so we return all results. + return tmpHashes + } + + // Remove/reduce matches in filteredHashes that were not found in this + // match (tmpHashes). + for k := range filteredHashes { + if tmpHashes[k] == nil { + delete(filteredHashes, k) + } + } + + return filteredHashes } -func (txi *TxIndex) matchRange(r queryRange, startKey []byte) (hashes [][]byte) { - // create a map to prevent duplicates - hashesMap := make(map[string][]byte) +// matchRange returns all matching txs by hash that meet a given queryRange and +// start key. An already filtered result (filteredHashes) is provided such that +// any non-intersecting matches are removed. +// +// NOTE: filteredHashes may be empty if no previous condition has matched. +func (txi *TxIndex) matchRange(r queryRange, startKey []byte, filteredHashes map[string][]byte, firstRun bool) map[string][]byte { + // A previous match was attempted but resulted in no matches, so we return + // no matches (assuming AND operand). + if !firstRun && len(filteredHashes) == 0 { + return filteredHashes + } + tmpHashes := make(map[string][]byte) lowerBound := r.lowerBoundValue() upperBound := r.upperBoundValue() it := dbm.IteratePrefix(txi.store, startKey) defer it.Close() + LOOP: for ; it.Valid(); it.Next() { if !isTagKey(it.Key()) { continue } + switch r.AnyBound().(type) { case int64: v, err := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) if err != nil { continue LOOP } + include := true if lowerBound != nil && v < lowerBound.(int64) { include = false } + if upperBound != nil && v > upperBound.(int64) { include = false } + if include { - hashesMap[fmt.Sprintf("%X", it.Value())] = it.Value() + tmpHashes[string(it.Value())] = it.Value() } + // XXX: passing time in a ABCI Tags is not yet implemented // case time.Time: // v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) @@ -418,13 +482,27 @@ LOOP: // } } } - hashes = make([][]byte, len(hashesMap)) - i := 0 - for _, h := range hashesMap { - hashes[i] = h - i++ + + if len(tmpHashes) == 0 || firstRun { + // Either: + // + // 1. Regardless if a previous match was attempted, which may have had + // results, but no match was found for the current condition, then we + // return no matches (assuming AND operand). + // + // 2. A previous match was not attempted, so we return all results. + return tmpHashes } - return + + // Remove/reduce matches in filteredHashes that were not found in this + // match (tmpHashes). + for k := range filteredHashes { + if tmpHashes[k] == nil { + delete(filteredHashes, k) + } + } + + return filteredHashes } /////////////////////////////////////////////////////////////////////////////// @@ -471,18 +549,3 @@ func startKey(fields ...interface{}) []byte { } return b.Bytes() } - -/////////////////////////////////////////////////////////////////////////////// -// Utils - -func intersect(as, bs [][]byte) [][]byte { - i := make([][]byte, 0, cmn.MinInt(len(as), len(bs))) - for _, a := range as { - for _, b := range bs { - if bytes.Equal(a, b) { - i = append(i, a) - } - } - } - return i -} diff --git a/state/txindex/kv/kv_bench_test.go b/state/txindex/kv/kv_bench_test.go new file mode 100644 index 000000000..9c3442a01 --- /dev/null +++ b/state/txindex/kv/kv_bench_test.go @@ -0,0 +1,72 @@ +package kv + +import ( + "crypto/rand" + "fmt" + "io/ioutil" + "testing" + + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/pubsub/query" + "github.com/tendermint/tendermint/types" + dbm "github.com/tendermint/tm-db" +) + +func BenchmarkTxSearch(b *testing.B) { + dbDir, err := ioutil.TempDir("", "benchmark_tx_search_test") + if err != nil { + b.Errorf("failed to create temporary directory: %s", err) + } + + db, err := dbm.NewGoLevelDB("benchmark_tx_search_test", dbDir) + if err != nil { + b.Errorf("failed to create database: %s", err) + } + + allowedTags := []string{"transfer.address", "transfer.amount"} + indexer := NewTxIndex(db, IndexTags(allowedTags)) + + for i := 0; i < 35000; i++ { + events := []abci.Event{ + { + Type: "transfer", + Attributes: []cmn.KVPair{ + {Key: []byte("address"), Value: []byte(fmt.Sprintf("address_%d", i%100))}, + {Key: []byte("amount"), Value: []byte("50")}, + }, + }, + } + + txBz := make([]byte, 8) + if _, err := rand.Read(txBz); err != nil { + b.Errorf("failed produce random bytes: %s", err) + } + + txResult := &types.TxResult{ + Height: int64(i), + Index: 0, + Tx: types.Tx(string(txBz)), + Result: abci.ResponseDeliverTx{ + Data: []byte{0}, + Code: abci.CodeTypeOK, + Log: "", + Events: events, + }, + } + + if err := indexer.Index(txResult); err != nil { + b.Errorf("failed to index tx: %s", err) + } + } + + txQuery := query.MustParse("transfer.address = 'address_43' AND transfer.amount = 50") + + b.ResetTimer() + + for i := 0; i < b.N; i++ { + if _, err := indexer.Search(txQuery); err != nil { + b.Errorf("failed to query for txs: %s", err) + } + } +} diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index cacfaad01..013e02b95 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -12,6 +12,8 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" db "github.com/tendermint/tendermint/libs/db" + abci "github.com/tendermint/tendermint/abci/types" + cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/state/txindex" "github.com/tendermint/tendermint/types" @@ -89,6 +91,11 @@ func TestTxSearch(t *testing.T) { {"account.number = 1 AND account.owner = 'Ivan'", 1}, // search by exact match (two tags) {"account.number = 1 AND account.owner = 'Vlad'", 0}, + {"account.owner = 'Vlad' AND account.number = 1", 0}, + {"account.number >= 1 AND account.owner = 'Vlad'", 0}, + {"account.owner = 'Vlad' AND account.number >= 1", 0}, + {"account.number <= 0", 0}, + {"account.number <= 0 AND account.owner = 'Ivan'", 0}, // search using a prefix of the stored value {"account.owner = 'Iv'", 0}, // search by range @@ -310,7 +317,7 @@ func benchmarkTxIndex(txsCount int64, b *testing.B) { } defer os.RemoveAll(dir) // nolint: errcheck - store := db.NewDB("tx_index", "leveldb", dir) + store := db.NewDB("tx_index", "goleveldb", dir) indexer := NewTxIndex(store) batch := txindex.NewBatch(txsCount) From 7bf891eff28b19f8e54045ebf78b058949e2182a Mon Sep 17 00:00:00 2001 From: Marko Baricevic Date: Wed, 31 Jul 2019 17:30:11 +0200 Subject: [PATCH 162/211] release for v0.32.2 --- CHANGELOG.md | 42 ++++++++++++++++++++++++++++++++++++++++-- CHANGELOG_PENDING.md | 24 +----------------------- version/version.go | 2 +- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cf2879c75..b34025213 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,48 @@ # Changelog +## v0.32.2 + +*July 31, 2019* + +Special thanks to external contributors on this release: +@ruseinov, @bluele, @guagualvcha + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### BREAKING CHANGES: + +- Go API + - [libs] [\#3811](https://github.com/tendermint/tendermint/issues/3811) Remove `db` from libs in favor of `https://github.com/tendermint/tm-db` + +### FEATURES: + +- [node] Allow replacing existing p2p.Reactor(s) using [`CustomReactors` + option](https://godoc.org/github.com/tendermint/tendermint/node#CustomReactors). + Warning: beware of accidental name clashes. Here is the list of existing + reactors: MEMPOOL, BLOCKCHAIN, CONSENSUS, EVIDENCE, PEX. + +### IMPROVEMENTS: + +- [p2p] [\#3834](https://github.com/tendermint/tendermint/issues/3834) Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file +- [abci] [\#3809](https://github.com/tendermint/tendermint/issues/3809) Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) +- [rpc] [\#2252](https://github.com/tendermint/tendermint/issues/2252) Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence +- [rpc] [\#3818](https://github.com/tendermint/tendermint/issues/3818) Make `max_body_bytes` and `max_header_bytes` configurable(@bluele) +- [p2p] [\#3664](https://github.com/tendermint/tendermint/issues/3664) p2p/conn: reuse buffer when write/read from secret connection(@guagualvcha) +- [mempool] [\#3826](https://github.com/tendermint/tendermint/issues/3826) Make `max_msg_bytes` configurable(@bluele) +- [blockchain] [\#3561](https://github.com/tendermint/tendermint/issues/3561) Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) +- [rpc] [\#3076](https://github.com/tendermint/tendermint/issues/3076) Improve transaction search performance + +### BUG FIXES: + +- [p2p] [\#3644](https://github.com/tendermint/tendermint/issues/3644) Fix error logging for connection stop (@defunctzombie) +- [rpc] [\#3813](https://github.com/tendermint/tendermint/issues/3813) Return err if page is incorrect (less than 0 or greater than total pages) + ## v0.32.1 *July 15, 2019* -Special thanks to external contributors on this release: +Special thanks to external contributors on this release: @ParthDesai, @climber73, @jim380, @ashleyvega This release contains a minor enhancement to the ABCI and some breaking changes to our libs folder, namely: @@ -26,7 +64,7 @@ program](https://hackerone.com/tendermint). ### FEATURES: -- [node] Add variadic argument to `NewNode` to support functional options, allowing the Node to be more easily customized. +- [node] Add variadic argument to `NewNode` to support functional options, allowing the Node to be more easily customized. - [node][\#3730](https://github.com/tendermint/tendermint/pull/3730) Add `CustomReactors` option to `NewNode` allowing caller to pass custom reactors to run inside Tendermint node (@ParthDesai) - [abci] [\#2127](https://github.com/tendermint/tendermint/issues/2127)RequestCheckTx has a new field, `CheckTxType`, which can take values of `CheckTxType_New` and `CheckTxType_Recheck`, indicating whether this is a new tx being checked for the first time or whether this tx is being rechecked after a block commit. This allows applications to skip certain expensive operations, like signature checking, if they've already been done once. see [docs](https://github.com/tendermint/tendermint/blob/eddb433d7c082efbeaf8974413a36641519ee895/docs/spec/abci/apps.md#mempool-connection) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 6488ffa90..f978d2dbc 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -1,4 +1,4 @@ -## v0.32.2 +## v0.32.3 \*\* @@ -9,30 +9,8 @@ program](https://hackerone.com/tendermint). ### BREAKING CHANGES: -- CLI/RPC/Config - -- Apps - -- Go API - ### FEATURES: -- [node] Allow replacing existing p2p.Reactor(s) using [`CustomReactors` - option](https://godoc.org/github.com/tendermint/tendermint/node#CustomReactors). - Warning: beware of accidental name clashes. Here is the list of existing - reactors: MEMPOOL, BLOCKCHAIN, CONSENSUS, EVIDENCE, PEX. ### IMPROVEMENTS: -<<<<<<< HEAD -======= -- [p2p] \#3834 Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file -- [abci] \#3809 Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) -- [rpc] \#2252 Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence -- [rpc] \#3818 Make `max_body_bytes` and `max_header_bytes` configurable -- [p2p] \#3664 p2p/conn: reuse buffer when write/read from secret connection -- [mempool] \#3826 Make `max_msg_bytes` configurable -- [blockchain] \#3561 Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) -- [rpc] \#3076 Improve transaction search performance - ->>>>>>> aacc71dc... txindexer: Refactor Tx Search Aggregation (#3851) ### BUG FIXES: diff --git a/version/version.go b/version/version.go index 91b0ab410..9fb7c7869 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.32.1" + TMCoreSemVer = "0.32.2" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.1" From 64f6034fbabe406e1eda017de5568eaa0b027578 Mon Sep 17 00:00:00 2001 From: Marko Date: Thu, 1 Aug 2019 18:33:27 +0200 Subject: [PATCH 163/211] Merge PR #3860: Update log v0.32.2 * changelog updates * pr comments --- CHANGELOG.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b34025213..398250d33 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,20 +17,20 @@ program](https://hackerone.com/tendermint). ### FEATURES: -- [node] Allow replacing existing p2p.Reactor(s) using [`CustomReactors` +- [node] [\#3846](https://github.com/tendermint/tendermint/pull/3846) Allow replacing existing p2p.Reactor(s) using [`CustomReactors` option](https://godoc.org/github.com/tendermint/tendermint/node#CustomReactors). Warning: beware of accidental name clashes. Here is the list of existing reactors: MEMPOOL, BLOCKCHAIN, CONSENSUS, EVIDENCE, PEX. +- [p2p] [\#3834](https://github.com/tendermint/tendermint/issues/3834) Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file +- [rpc] [\#3818](https://github.com/tendermint/tendermint/issues/3818) Make `max_body_bytes` and `max_header_bytes` configurable(@bluele) +- [mempool] [\#3826](https://github.com/tendermint/tendermint/issues/3826) Make `max_msg_bytes` configurable(@bluele) +- [blockchain] [\#3561](https://github.com/tendermint/tendermint/issues/3561) Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) ### IMPROVEMENTS: -- [p2p] [\#3834](https://github.com/tendermint/tendermint/issues/3834) Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file - [abci] [\#3809](https://github.com/tendermint/tendermint/issues/3809) Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) - [rpc] [\#2252](https://github.com/tendermint/tendermint/issues/2252) Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence -- [rpc] [\#3818](https://github.com/tendermint/tendermint/issues/3818) Make `max_body_bytes` and `max_header_bytes` configurable(@bluele) - [p2p] [\#3664](https://github.com/tendermint/tendermint/issues/3664) p2p/conn: reuse buffer when write/read from secret connection(@guagualvcha) -- [mempool] [\#3826](https://github.com/tendermint/tendermint/issues/3826) Make `max_msg_bytes` configurable(@bluele) -- [blockchain] [\#3561](https://github.com/tendermint/tendermint/issues/3561) Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) - [rpc] [\#3076](https://github.com/tendermint/tendermint/issues/3076) Improve transaction search performance ### BUG FIXES: From c3ef900c4243f4611bf7c9b6a5667469f5a2bfb5 Mon Sep 17 00:00:00 2001 From: zjubfd <296179868@qq.com> Date: Tue, 10 Sep 2019 11:05:18 +0800 Subject: [PATCH 164/211] Revert "[R4R] prepare for release v0.31.5-binance.2 (#113)" (#117) This reverts commit 17757a0b382d2eb7d7f102d9944936966c844297. --- CHANGELOG.md | 9 - blockchain/hot/candidate.go | 413 ---------- blockchain/hot/candidate_test.go | 251 ------ blockchain/hot/metrics.go | 120 --- blockchain/hot/pool.go | 1091 --------------------------- blockchain/hot/pool_test.go | 604 --------------- blockchain/hot/reactor.go | 365 --------- blockchain/hot/reactor_test.go | 260 ------- blockchain/hot/wire.go | 13 - blockchain/pool.go | 2 +- blockchain/reactor.go | 68 +- blockchain/reactor_test.go | 17 +- blockchain/store_test.go | 15 - config/config.go | 44 +- config/toml.go | 17 +- consensus/reactor.go | 7 - consensus/state.go | 6 - consensus/types/round_state.go | 1 - mempool/mempool.go | 29 +- mempool/reactor.go | 56 +- node/node.go | 108 +-- p2p/conn/connection.go | 3 + p2p/peer.go | 7 + p2p/switch.go | 44 +- p2p/switch_test.go | 2 - rpc/core/pipe.go | 5 - rpc/core/status.go | 4 +- rpc/core/types/responses.go | 1 - snapshot/reactor.go | 28 +- state/blockindex/indexer_service.go | 9 - state/execution.go | 14 +- state/index.go | 181 ----- state/index_test.go | 90 --- state/metrics.go | 9 - state/store.go | 2 +- state/txindex/indexer_service.go | 9 - types/event_bus.go | 4 - types/events.go | 11 - types/vote_set.go | 15 - 39 files changed, 183 insertions(+), 3751 deletions(-) delete mode 100644 blockchain/hot/candidate.go delete mode 100644 blockchain/hot/candidate_test.go delete mode 100644 blockchain/hot/metrics.go delete mode 100644 blockchain/hot/pool.go delete mode 100644 blockchain/hot/pool_test.go delete mode 100644 blockchain/hot/reactor.go delete mode 100644 blockchain/hot/reactor_test.go delete mode 100644 blockchain/hot/wire.go delete mode 100644 state/index.go delete mode 100644 state/index_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index ba671cdf0..a4a1c2e66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,5 @@ # Changelog -## v0.31.5-binance.2 -*Sep 6th, 2019* -### FEATURES: -- [sync] [\#97](https://github.com/binance-chain/bnc-tendermint/pull/97) supoort hot sync reactor - -### IMPROVEMENTS: -- [index] [\#106](https://github.com/binance-chain/bnc-tendermint/pull/106) index service recover from data lost -- [P2P] [\#106](https://github.com/binance-chain/bnc-tendermint/pull/107) introduce skip_tx_from_persistent config and other basic p2p improvement - ## v0.31.5-binance.1 *July 17th, 2019* diff --git a/blockchain/hot/candidate.go b/blockchain/hot/candidate.go deleted file mode 100644 index 9e4f03a44..000000000 --- a/blockchain/hot/candidate.go +++ /dev/null @@ -1,413 +0,0 @@ -package hot - -import ( - "container/list" - "math/rand" - "sort" - "sync" - "time" - - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/p2p" -) - -const ( - // the type of samples, Good means get sample result in limit time, otherwise is Bad event. - Bad eventType = iota - Good -) - -const ( - // the interval to recalculate average metrics, 10 is reasonable according to test. - recalculateInterval = 10 - // the interval to pick decay peers - pickDecayPeerInterval = 5000 - // only maxPermanentSetSize of best peers will stay in permanentSet - maxPermanentSetSize = 2 - // only recent maxMetricsSampleSize of samples will be used to calculate the metrics - maxMetricsSampleSize = 100 - - // the time interval to keep consistent with peerSet in Switch - compensateInterval = 2 -) - -type eventType uint - -type CandidatePool struct { - cmn.BaseService - mtx sync.RWMutex - random *rand.Rand - - pickSequence int64 - - // newly added peer, will be candidates in next pick round. - freshSet map[p2p.ID]struct{} - // unavailable or poor performance peers, give a try periodically - decayedSet map[p2p.ID]struct{} - // stable and good performance peers - permanentSet map[p2p.ID]*peerMetrics - - eventStream <-chan metricsEvent - - metrics *Metrics - swh *p2p.Switch -} - -func NewCandidatePool(eventStream <-chan metricsEvent) *CandidatePool { - random := rand.New(rand.NewSource(time.Now().Unix())) - c := &CandidatePool{ - eventStream: eventStream, - random: random, - // a random init value to avoid network resource race. - pickSequence: random.Int63n(pickDecayPeerInterval), - freshSet: make(map[p2p.ID]struct{}, 0), - decayedSet: make(map[p2p.ID]struct{}, 0), - permanentSet: make(map[p2p.ID]*peerMetrics, maxPermanentSetSize), - metrics: NopMetrics(), - } - c.BaseService = *cmn.NewBaseService(nil, "CandidatePool", c) - return c -} - -// OnStart implements cmn.Service. -func (c *CandidatePool) OnStart() error { - go c.handleSampleRoutine() - go c.compensatePeerRoutine() - - return nil -} - -func (c *CandidatePool) addPeer(pid p2p.ID) { - c.mtx.Lock() - defer c.mtx.Unlock() - c.freshSet[pid] = struct{}{} -} - -func (c *CandidatePool) removePeer(pid p2p.ID) { - c.mtx.Lock() - defer c.mtx.Unlock() - delete(c.freshSet, pid) - c.tryRemoveFromDecayed(pid) - c.tryRemoveFromPermanent(pid) -} - -func (c *CandidatePool) PickCandidates() []*p2p.ID { - c.mtx.Lock() - defer c.mtx.Unlock() - c.pickSequence = c.pickSequence + 1 - permanentPeer := c.pickFromPermanentPeer() - // if there is no permanentPeer, need broadcast in decayed peers. - isBroadcast := permanentPeer == nil - decaysPeers := c.pickFromDecayedSet(isBroadcast) - freshPeers := c.pickFromFreshSet() - peers := make([]*p2p.ID, 0, 1) - if permanentPeer != nil { - peers = append(peers, permanentPeer) - } - peers = append(peers, decaysPeers...) - peers = append(peers, freshPeers...) - return peers -} - -// The `AddPeer` and `RemovePeer` function of BaseReactor have concurrent issue: -// a peer is removed and immediately added, `AddPeer` may execute before `RemovePeer`. -// Can't fix this in `stopAndRemovePeer`, because may introduce other peer dial failed. -// For `CandidatePool` it is really important to keep consistent with peerset in switch, -// this routine is an insurance. -func (c *CandidatePool) compensatePeerRoutine() { - if c.swh == nil { - // happened in test - return - } - compensateTicker := time.NewTicker(compensateInterval) - defer compensateTicker.Stop() - for { - select { - case <-c.Quit(): - return - case <-compensateTicker.C: - func() { - c.mtx.Lock() - defer c.mtx.Unlock() - peers := c.swh.Peers().List() - for _, p := range peers { - if !c.exist(p.ID()) { - c.freshSet[p.ID()] = struct{}{} - } - } - for p := range c.permanentSet { - if !c.swh.Peers().Has(p) { - c.tryRemoveFromPermanent(p) - } - } - for p := range c.decayedSet { - if !c.swh.Peers().Has(p) { - c.tryRemoveFromDecayed(p) - } - } - for p := range c.freshSet { - if !c.swh.Peers().Has(p) { - delete(c.freshSet, p) - } - } - c.metrics.DecayPeerSetSize.Set(float64(len(c.decayedSet))) - c.metrics.PermanentPeerSetSize.Set(float64(len(c.permanentSet))) - }() - } - } -} - -func (c *CandidatePool) handleSampleRoutine() { - for { - select { - case <-c.Quit(): - return - case e := <-c.eventStream: - c.handleMetricsEvent(e) - } - } -} - -func (c *CandidatePool) handleMetricsEvent(e metricsEvent) { - c.mtx.Lock() - defer c.mtx.Unlock() - pid := e.pid - if !c.exist(pid) { - c.Logger.Debug("the peer is already removed", "peer", e.pid, "type", e.et) - return - } - if e.et == Bad { - if _, exist := c.decayedSet[pid]; !exist { - // just try to delete it, no need to check if it exist. - delete(c.freshSet, pid) - c.tryRemoveFromPermanent(pid) - c.decayedSet[pid] = struct{}{} - c.metrics.DecayPeerSetSize.Add(1) - } else { - // already in decayed set, nothing need to do. - } - } else if e.et == Good { - isFresh := c.isFresh(pid) - isDecayed := c.isDecayed(pid) - if isFresh || isDecayed { - if isFresh { - delete(c.freshSet, pid) - } - if isDecayed { - c.tryRemoveFromDecayed(pid) - } - metrics := newPeerMetrics(c.random) - metrics.addSample(e.dur) - added, kickPeer := c.tryAddPermanentPeer(pid, metrics) - if added { - c.Logger.Debug("new peer joined permanent peer set", "peer", pid) - } - // if some peers been kicked out, just join decayed peers. - if kickPeer != nil { - c.decayedSet[*kickPeer] = struct{}{} - c.metrics.DecayPeerSetSize.Add(1) - } - } else if metrics, exist := c.permanentSet[pid]; exist { - metrics.addSample(e.dur) - } else { - // should not happen - c.Logger.Error("receive event from peer which belongs to no peer set", "peer", e.pid) - } - } else { - c.Logger.Error("receive unknown metrics event type", "type", e.et) - } -} - -func (c *CandidatePool) tryAddPermanentPeer(pid p2p.ID, metrics peerMetrics) (bool, *p2p.ID) { - if _, exist := c.permanentSet[pid]; exist { - c.Logger.Error("try to insert a metrics that already exists", "peer", pid) - // unexpected things happened, the best choice is degrade this peer and try to fix. - c.tryRemoveFromPermanent(pid) - return false, &pid - } - if len(c.permanentSet) >= maxPermanentSetSize { - //try to kick out the worst candidate. - maxDelay := metrics.average - kickPeer := pid - for p, m := range c.permanentSet { - if maxDelay < m.average { - maxDelay = m.average - kickPeer = p - } - } - if kickPeer != pid { - c.tryRemoveFromPermanent(kickPeer) - c.permanentSet[pid] = &metrics - c.metrics.PermanentPeerSetSize.Add(1) - c.metrics.PermanentPeers.With("peer_id", string(pid)).Set(1) - return true, &kickPeer - } - return false, &pid - } else { - c.permanentSet[pid] = &metrics - c.metrics.PermanentPeerSetSize.Add(1) - c.metrics.PermanentPeers.With("peer_id", string(pid)).Set(1) - return true, nil - } -} - -func (c *CandidatePool) pickFromFreshSet() []*p2p.ID { - peers := make([]*p2p.ID, 0, len(c.freshSet)) - for peer := range c.freshSet { - // use a temp var to avoid append a same point. - tmpPeer := peer - // move to decayed once picked - if _, exist := c.decayedSet[tmpPeer]; !exist { - c.decayedSet[tmpPeer] = struct{}{} - c.metrics.DecayPeerSetSize.Add(float64(1)) - } - peers = append(peers, &tmpPeer) - } - // clean fresh set - c.freshSet = make(map[p2p.ID]struct{}, 0) - return peers -} - -func (c *CandidatePool) pickFromDecayedSet(broadcast bool) []*p2p.ID { - if len(c.decayedSet) == 0 { - return []*p2p.ID{} - } - peers := make([]*p2p.ID, 0, len(c.decayedSet)) - for peer := range c.decayedSet { - // use a temp var to avoid append a same point. - tmpPeer := peer - peers = append(peers, &tmpPeer) - } - if !broadcast { - if c.pickSequence%pickDecayPeerInterval == 0 { - index := c.random.Intn(len(peers)) - return []*p2p.ID{peers[index]} - } - return []*p2p.ID{} - } else { - return peers - } -} - -// larger average while less opportunity to be chosen. example: -// peer : peerA peerB peerC -// average delay: 1ns 1ns 8ns -// percentage : 0.1 0.1 0.8 -// diceSection : [0.9, 1.8, 2.0] -// choose opportunity: 45%, 45%, 10% -func (c *CandidatePool) pickFromPermanentPeer() *p2p.ID { - size := len(c.permanentSet) - if size == 0 { - return nil - } - diceSection := make([]float64, 0, size) - peers := make([]p2p.ID, 0, size) - var total, section float64 - for _, m := range c.permanentSet { - total += float64(m.average) - } - // total could not be 0 actually, but assign with 1 if it unfortunately happened - if total == 0 { - total = 1 - } - for p, m := range c.permanentSet { - peers = append(peers, p) - section = section + (1 - float64(m.average)/total) - diceSection = append(diceSection, section) - } - diceValue := c.random.Float64() * diceSection[len(diceSection)-1] - choose := sort.SearchFloat64s(diceSection, diceValue) - return &peers[choose] -} - -func (c *CandidatePool) exist(pid p2p.ID) bool { - if c.isPermanent(pid) { - return true - } else if c.isDecayed(pid) { - return true - } else if c.isFresh(pid) { - return true - } - return false -} - -func (c *CandidatePool) isFresh(pid p2p.ID) bool { - _, exist := c.freshSet[pid] - return exist -} - -func (c *CandidatePool) isDecayed(pid p2p.ID) bool { - _, exist := c.decayedSet[pid] - return exist -} - -func (c *CandidatePool) isPermanent(pid p2p.ID) bool { - _, exist := c.permanentSet[pid] - return exist -} - -func (c *CandidatePool) tryRemoveFromPermanent(pid p2p.ID) { - if c.isPermanent(pid) { - delete(c.permanentSet, pid) - c.metrics.PermanentPeerSetSize.Set(-1) - c.metrics.PermanentPeers.With("peer_id", string(pid)).Set(0) - } -} - -func (c *CandidatePool) tryRemoveFromDecayed(pid p2p.ID) { - if c.isDecayed(pid) { - delete(c.decayedSet, pid) - c.metrics.DecayPeerSetSize.Set(-1) - } -} - -// -------------------------------------------------- -type metricsEvent struct { - et eventType - pid p2p.ID - dur int64 // nano second -} - -type peerMetrics struct { - sampleSequence int64 - - samples *list.List - average int64 // nano second -} - -func newPeerMetrics(random *rand.Rand) peerMetrics { - return peerMetrics{ - samples: list.New(), - sampleSequence: random.Int63n(recalculateInterval), - } -} - -func (metrics *peerMetrics) addSample(dur int64) { - metrics.sampleSequence = metrics.sampleSequence + 1 - // fast calculate average, but error accumulates - if metrics.samples.Len() >= maxMetricsSampleSize { - // shift old sample - popItem := metrics.samples.Front() - metrics.samples.Remove(popItem) - popDur := *popItem.Value.(*int64) - // no worry about int64 overflow, the max dur will not exceed math.MaxInt64/maxMetricsSampleSize - metrics.average = (metrics.average*int64(maxMetricsSampleSize) + (dur - popDur)) / int64(maxMetricsSampleSize) - } else { - length := int64(metrics.samples.Len()) - metrics.average = (metrics.average*length + dur) / (length + 1) - } - metrics.samples.PushBack(&dur) - - // correction error periodically - if metrics.sampleSequence%recalculateInterval == 0 { - var totalDur int64 - // no worry about int64 overflow, the max dur will not exceed math.MaxInt64/maxMetricsSampleSize - s := metrics.samples.Front() - for s != nil { - sdur := *s.Value.(*int64) - totalDur += sdur - s = s.Next() - } - metrics.average = totalDur / int64(metrics.samples.Len()) - } -} diff --git a/blockchain/hot/candidate_test.go b/blockchain/hot/candidate_test.go deleted file mode 100644 index d76665479..000000000 --- a/blockchain/hot/candidate_test.go +++ /dev/null @@ -1,251 +0,0 @@ -package hot - -import ( - "fmt" - "math" - "math/rand" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/tendermint/tendermint/p2p" -) - -func makeTestPeerId(num int) []p2p.ID { - if num <= 0 { - return []p2p.ID{} - } - pids := make([]p2p.ID, 0, num) - for i := 0; i < num; i++ { - pids = append(pids, p2p.ID(fmt.Sprintf("test id %v", i))) - } - return pids -} -func randomEvent() eventType { - if rand.Int()%2 == 0 { - return Good - } else { - return Bad - } -} -func TestPeerMetricsBasic(t *testing.T) { - pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) - seq := pm.sampleSequence - sample := rand.Int63() - pm.addSample(sample) - - assert.Equal(t, pm.average, sample) - assert.Equal(t, pm.samples.Len(), 1) - assert.Equal(t, pm.sampleSequence, seq+1) -} - -func TestPeerMetricsNoOverflow(t *testing.T) { - pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) - for i := 0; i < 10000; i++ { - // make sure it will not cause math overflow. - sample := rand.Int63n(math.MaxInt64 / maxMetricsSampleSize) - pm.addSample(sample) - } - assert.Equal(t, pm.samples.Len(), maxMetricsSampleSize) -} - -func TestPeerMetricsParamResonableInMillisecondLevel(t *testing.T) { - var sum int64 - pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) - for i := 0; i < recalculateInterval-1; i++ { - sample := rand.Int63n(recalculateInterval-1) * time.Millisecond.Nanoseconds() - sum += sample - pm.addSample(sample) - } - average := sum / (recalculateInterval - 1) - // the diff is not too much - assert.True(t, (average-pm.average) < time.Millisecond.Nanoseconds()/10 && (average-pm.average) < time.Millisecond.Nanoseconds()/10) - // the average is not too small - assert.True(t, average > 100) -} - -func TestPeerMetricsNoErrorAccumulate(t *testing.T) { - pm := newPeerMetrics(rand.New(rand.NewSource(time.Now().Unix()))) - for i := 0; i < 10000; i++ { - sample := rand.Int63n(math.MaxInt64 / maxMetricsSampleSize) - pm.addSample(sample) - } - var sum int64 - // make sure it will recalculate when finish - pm.sampleSequence = 0 - for i := 0; i < maxMetricsSampleSize; i++ { - sample := rand.Int63n(math.MaxInt64 / maxMetricsSampleSize) - sum += sample - pm.addSample(sample) - } - average := sum / maxMetricsSampleSize - assert.Equal(t, pm.average, average) -} - -func TestCandidatePoolBasic(t *testing.T) { - sampleStream := make(chan metricsEvent) - candidatePool := NewCandidatePool(sampleStream) - candidatePool.Start() - defer candidatePool.Stop() - - // control the pick decay logic - candidatePool.pickSequence = 0 - totalPidNum := 85 - goodPidNum := 21 - - testPids := makeTestPeerId(totalPidNum) - for _, p := range testPids { - candidatePool.addPeer(p) - } - // peers stay in fresh set until an event come in - for i := 0; i < 2; i++ { - candidates := candidatePool.PickCandidates() - assert.Equal(t, len(candidates), totalPidNum) - } - - for i := 0; i < goodPidNum; i++ { - sampleStream <- metricsEvent{Good, testPids[i], int64(i) * time.Millisecond.Nanoseconds()} - } - for i := goodPidNum; i < totalPidNum; i++ { - sampleStream <- metricsEvent{Bad, testPids[i], 0} - } - //wait for pool to handle - time.Sleep(10 * time.Millisecond) - for i := 0; i < 2; i++ { - candidates := candidatePool.PickCandidates() - // only one peer is selected - assert.Equal(t, len(candidates), 1) - } - assert.Equal(t, len(candidatePool.permanentSet), maxPermanentSetSize) - for i := 0; i < maxPermanentSetSize; i++ { - _, exist := candidatePool.permanentSet[testPids[i]] - assert.True(t, exist) - } - assert.Equal(t, len(candidatePool.decayedSet), totalPidNum-maxPermanentSetSize) - assert.Equal(t, len(candidatePool.freshSet), 0) -} - -func TestCandidatePoolPickDecayPeriodically(t *testing.T) { - sampleStream := make(chan metricsEvent) - candidatePool := NewCandidatePool(sampleStream) - candidatePool.Start() - defer candidatePool.Stop() - testPids := makeTestPeerId(2) - candidatePool.addPeer(testPids[0]) - candidatePool.addPeer(testPids[1]) - sampleStream <- metricsEvent{Good, testPids[0], 1 * time.Millisecond.Nanoseconds()} - sampleStream <- metricsEvent{Bad, testPids[1], 0} - - //wait for pool to handle - time.Sleep(10 * time.Millisecond) - // control the pick decay logic - candidatePool.pickSequence = 0 - - for i := 0; i < pickDecayPeerInterval-1; i++ { - peers := candidatePool.PickCandidates() - assert.Equal(t, len(peers), 1) - } - peers := candidatePool.PickCandidates() - assert.Equal(t, len(peers), 2) -} - -func TestCandidatePoolNoDuplicatePeer(t *testing.T) { - sampleStream := make(chan metricsEvent) - candidatePool := NewCandidatePool(sampleStream) - candidatePool.Start() - defer candidatePool.Stop() - totalPidNum := 1000 - testPids := makeTestPeerId(totalPidNum) - for _, p := range testPids { - candidatePool.addPeer(p) - } - for i := 0; i < 100000; i++ { - dur := rand.Int63() - et := randomEvent() - peer := testPids[rand.Intn(totalPidNum)] - sampleStream <- metricsEvent{et, peer, dur} - } - time.Sleep(10 * time.Millisecond) - assert.Equal(t, len(candidatePool.freshSet)+len(candidatePool.decayedSet)+len(candidatePool.permanentSet), totalPidNum) -} - -func TestCandidatePoolPickInScore(t *testing.T) { - sampleStream := make(chan metricsEvent) - candidatePool := NewCandidatePool(sampleStream) - candidatePool.Start() - defer candidatePool.Stop() - totalPidNum := maxPermanentSetSize - testPids := makeTestPeerId(totalPidNum) - for i, p := range testPids { - candidatePool.addPeer(p) - sampleStream <- metricsEvent{Good, p, int64(i+1) * time.Millisecond.Nanoseconds()} - } - time.Sleep(10 * time.Millisecond) - candidatePool.PickCandidates() - pickRate := make(map[p2p.ID]int) - for i := 0; i < 100000; i++ { - peers := candidatePool.PickCandidates() - assert.Equal(t, 1, len(peers)) - peer := *peers[0] - pickRate[peer] = pickRate[peer] + 1 - } - for i := 0; i < maxPermanentSetSize; i++ { - fmt.Printf("index %d rate is %v\n", i, float64(pickRate[testPids[i]])/float64(100000)) - } - for i := 0; i < maxPermanentSetSize-1; i++ { - assert.True(t, pickRate[testPids[i]] > pickRate[testPids[i+1]]) - } -} - -func TestPickFromFreshSet(t *testing.T) { - sampleStream := make(chan metricsEvent) - candidatePool := NewCandidatePool(sampleStream) - candidatePool.Start() - defer candidatePool.Stop() - testPids := makeTestPeerId(100) - for _, p := range testPids { - candidatePool.addPeer(p) - } - candidates := candidatePool.pickFromFreshSet() - for _, c := range candidates { - for idx, tpid := range testPids { - if *c == tpid { - if len(testPids) > 1 { - testPids = append(testPids[:idx], testPids[idx+1:]...) - break - } else { - testPids = nil - break - } - } - } - } - assert.Nil(t, testPids) -} - -func TestPickFromDecayedSet(t *testing.T) { - sampleStream := make(chan metricsEvent) - candidatePool := NewCandidatePool(sampleStream) - candidatePool.Start() - defer candidatePool.Stop() - testPids := makeTestPeerId(100) - for _, p := range testPids { - candidatePool.addPeer(p) - sampleStream <- metricsEvent{Bad, p, 0} - } - candidates := candidatePool.pickFromDecayedSet(true) - for _, c := range candidates { - for idx, tpid := range testPids { - if *c == tpid { - if len(testPids) > 1 { - testPids = append(testPids[:idx], testPids[idx+1:]...) - break - } else { - testPids = nil - break - } - } - } - } - assert.Nil(t, testPids) -} diff --git a/blockchain/hot/metrics.go b/blockchain/hot/metrics.go deleted file mode 100644 index 9617ebf86..000000000 --- a/blockchain/hot/metrics.go +++ /dev/null @@ -1,120 +0,0 @@ -package hot - -import ( - "github.com/go-kit/kit/metrics" - "github.com/go-kit/kit/metrics/discard" - "github.com/go-kit/kit/metrics/prometheus" - stdprometheus "github.com/prometheus/client_golang/prometheus" -) - -const ( - // MetricsSubsystem is a subsystem shared by all metrics exposed by this - // package. - MetricsSubsystem = "hot_sync" -) - -// Metrics contains metrics exposed by this package. -type Metrics struct { - // Height of the chain. - Height metrics.Gauge - // Time between this and the last block. - BlockIntervalSeconds metrics.Gauge - // Number of transactions. - NumTxs metrics.Gauge - // Size of the block. - BlockSizeBytes metrics.Gauge - // Total number of transactions. - TotalTxs metrics.Gauge - // The latest block height. - CommittedHeight metrics.Gauge - - // The number of peers consider as good - PermanentPeerSetSize metrics.Gauge - // The detail peers consider as good - PermanentPeers metrics.Gauge - // The number of peers consider as bad - DecayPeerSetSize metrics.Gauge -} - -// PrometheusMetrics returns Metrics build using Prometheus client library. -// Optionally, labels can be provided along with their values ("foo", -// "fooValue"). -func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { - labels := []string{} - for i := 0; i < len(labelsAndValues); i += 2 { - labels = append(labels, labelsAndValues[i]) - } - return &Metrics{ - Height: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "height", - Help: "Height of the chain.", - }, labels).With(labelsAndValues...), - - BlockIntervalSeconds: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "block_interval_seconds", - Help: "Time between this and the last block.", - }, labels).With(labelsAndValues...), - - NumTxs: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "num_txs", - Help: "Number of transactions.", - }, labels).With(labelsAndValues...), - BlockSizeBytes: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "block_size_bytes", - Help: "Size of the block.", - }, labels).With(labelsAndValues...), - TotalTxs: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "total_txs", - Help: "Total number of transactions.", - }, labels).With(labelsAndValues...), - CommittedHeight: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "latest_block_height", - Help: "The latest block height.", - }, labels).With(labelsAndValues...), - PermanentPeerSetSize: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "permanent_set_size", - Help: "The number of peers consider as good.", - }, labels).With(labelsAndValues...), - DecayPeerSetSize: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "decay_peer_set_size", - Help: "The number of peers consider as bad", - }, labels).With(labelsAndValues...), - PermanentPeers: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "permanent_peers", - Help: "The details of peers consider as bad", - }, append(labels, "peer_id")).With(labelsAndValues...), - } -} - -// NopMetrics returns no-op Metrics. -func NopMetrics() *Metrics { - return &Metrics{ - Height: discard.NewGauge(), - BlockIntervalSeconds: discard.NewGauge(), - NumTxs: discard.NewGauge(), - BlockSizeBytes: discard.NewGauge(), - TotalTxs: discard.NewGauge(), - CommittedHeight: discard.NewGauge(), - PermanentPeerSetSize: discard.NewGauge(), - PermanentPeers: discard.NewGauge(), - DecayPeerSetSize: discard.NewGauge(), - } -} diff --git a/blockchain/hot/pool.go b/blockchain/hot/pool.go deleted file mode 100644 index 007351d36..000000000 --- a/blockchain/hot/pool.go +++ /dev/null @@ -1,1091 +0,0 @@ -package hot - -import ( - "bytes" - "context" - "fmt" - "sync" - "sync/atomic" - "time" - - "github.com/tendermint/tendermint/blockchain" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - st "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -const ( - // SyncPattern is the work pattern of BlockPool. - // 1. Mute: will only answer subscribe requests from others, will not sync from others or from consensus channel. - // 2. Hot: handle subscribe requests from other peer as a publisher, also subscribe block messages - // from other peers as a subscriber. - // 3. Consensus: handle subscribe requests from other peer as a publisher, but subscribe block message from - // consensus channel. - // The viable transitions between are: - // Hot --> Consensus - // ^ ^ - // | / - // | / - // Mute - Mute SyncPattern = iota - Hot - Consensus -) - -const ( - // the time interval to correct current state. - tryRepairInterval = 1 * time.Second - - maxCachedSealedBlock = 100 - - // the max num of blocks can subscribed in advance. - maxSubscribeForesight = 40 - // the max num of blocks other peers can subscribe from us ahead of current height. - // should greater than maxSubscribeForesight. - maxPublishForesight = 80 - - maxCapPubFromStore = 1000 - - eventBusSubscribeCap = 1000 - - subscriber = "HotSyncService" - selfId = p2p.ID("self") -) - -type SyncPattern uint - -type BlockPool struct { - cmn.BaseService - mtx sync.Mutex - st SyncPattern - eventBus *types.EventBus - store *blockchain.BlockStore - blockExec *st.BlockExecutor - - blockTimeout time.Duration - - //--- state --- - // the verified blockState received recently - blockStates map[int64]*blockState - subscriberPeerSets map[int64]map[p2p.ID]struct{} - state st.State - blocksSynced int32 - subscribedHeight int64 - height int64 - - //--- internal communicate --- - blockStateSealCh chan *blockState - publisherStateSealCh chan *publisherState - decayedPeers chan decayedPeer - sendCh chan<- Message - pubFromStoreCh chan subFromStoreMsg - - //--- peer metrics --- - candidatePool *CandidatePool - sampleStream chan<- metricsEvent - - //--- other metrics -- - metrics *Metrics - - //--- for switch --- - switchCh chan struct{} - switchWg sync.WaitGroup -} - -func NewBlockPool(store *blockchain.BlockStore, blockExec *st.BlockExecutor, state st.State, sendCh chan<- Message, st SyncPattern, blockTimeout time.Duration) *BlockPool { - const capacity = 1000 - sampleStream := make(chan metricsEvent, capacity) - candidates := NewCandidatePool(sampleStream) - bp := &BlockPool{ - store: store, - height: state.LastBlockHeight, - blockExec: blockExec, - state: state, - subscribedHeight: state.LastBlockHeight, - blockStates: make(map[int64]*blockState, maxCachedSealedBlock), - subscriberPeerSets: make(map[int64]map[p2p.ID]struct{}, 0), - publisherStateSealCh: make(chan *publisherState, capacity), - sendCh: sendCh, - candidatePool: candidates, - sampleStream: sampleStream, - switchCh: make(chan struct{}, 1), - decayedPeers: make(chan decayedPeer, capacity), - blockStateSealCh: make(chan *blockState, capacity), - blockTimeout: blockTimeout, - st: st, - metrics: NopMetrics(), - pubFromStoreCh: make(chan subFromStoreMsg, maxCapPubFromStore), - } - bp.BaseService = *cmn.NewBaseService(nil, "HotBlockPool", bp) - return bp -} - -func (pool *BlockPool) setEventBus(eventBus *types.EventBus) { - pool.eventBus = eventBus -} - -func (pool *BlockPool) setMetrics(metrics *Metrics) { - pool.metrics = metrics - pool.candidatePool.metrics = metrics -} - -//---------------Service implement ---------------- -func (pool *BlockPool) OnStart() error { - if pool.st == Consensus { - pool.Logger.Info("block pool start in consensus pattern") - go pool.consensusSyncRoutine() - } else if pool.st == Hot { - pool.Logger.Info("block pool start in hotSync pattern") - pool.switchWg.Add(1) - pool.candidatePool.Start() - go pool.hotSyncRoutine() - } else { - pool.Logger.Info("block pool start in mute pattern") - } - go pool.pubFromStoreRoutine() - return nil -} - -func (pool *BlockPool) OnStop() { - if pool.candidatePool.IsRunning() { - pool.candidatePool.Stop() - } -} - -func (pool *BlockPool) SetLogger(l log.Logger) { - pool.BaseService.Logger = l.With("module", "blockPool") - pool.candidatePool.SetLogger(l.With("module", "candidatePool")) -} - -func (pool *BlockPool) AddPeer(peer p2p.Peer) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - // no matter what sync pattern now, need maintain candidatePool. - pool.candidatePool.addPeer(peer.ID()) -} - -func (pool *BlockPool) RemovePeer(peer p2p.Peer) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - // no matter what sync pattern now, need maintain candidatePool. - pool.candidatePool.removePeer(peer.ID()) - if pool.st == Hot { - pool.decayedPeers <- decayedPeer{peerId: peer.ID()} - } - for _, subscribers := range pool.subscriberPeerSets { - delete(subscribers, peer.ID()) - } -} - -// ------ switch logic ----- -func (pool *BlockPool) SwitchToHotSync(state st.State, blockSynced int32) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - if pool.st != Mute { - panic(fmt.Errorf("can't switch to hotsync pattern, current sync pattern is %d", pool.st)) - } - pool.Logger.Info("switch to hot sync pattern") - pool.st = Hot - pool.state = state - pool.subscribedHeight = state.LastBlockHeight - pool.blocksSynced = blockSynced - pool.height = state.LastBlockHeight - - pool.switchWg.Add(1) - pool.candidatePool.Start() - go pool.hotSyncRoutine() -} - -func (pool *BlockPool) SwitchToConsensusSync(state *st.State) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - if pool.st == Consensus { - panic("already in consensus sync, can't switch to consensus sync") - } - var copyState st.State - // means switch from hot sync - if state == nil { - copyState = pool.state - } else { - copyState = state.Copy() - } - pool.switchCh <- struct{}{} - // wait until hotSyncRoutine ends. - pool.switchWg.Wait() - // clean block states from expecting height. - pool.resetBlockStates() - - pool.Logger.Info("switch to consensus sync pattern") - pool.st = Consensus - pool.state = copyState - pool.height = copyState.LastBlockHeight - pool.subscribedHeight = copyState.LastBlockHeight - - if pool.candidatePool.IsRunning() { - pool.candidatePool.Stop() - } - go pool.consensusSyncRoutine() -} - -//------- handle request from other peer ----- -func (pool *BlockPool) handleSubscribeBlock(fromHeight, toHeight int64, src p2p.Peer) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - if pool.st == Mute { - for height := fromHeight; height <= toHeight; height++ { - if height < pool.store.Height() { - select { - case pool.pubFromStoreCh <- subFromStoreMsg{height: height, src: src}: - default: - pool.Logger.Error("the pubFromStoreCh channel is full, skip loading block from store", "peer", src.ID(), "height", height) - return - } - } else { - // Won't answer no block message immediately. - // Afraid of the peer will subscribe again immediately, which - // will cause message burst. - return - } - } - return - } - for height := fromHeight; height <= toHeight; height++ { - blockState := pool.blockStates[height] - if blockState != nil && blockState.isSealed() { - pool.sendMessages(src.ID(), &blockCommitResponseMessage{blockState.block, blockState.commit}) - } else if height <= pool.currentHeight() { - select { - case pool.pubFromStoreCh <- subFromStoreMsg{height: height, src: src}: - default: - pool.Logger.Error("the pubFromStoreCh channel is full, skip loading block from store", "peer", src.ID(), "height", height) - return - } - } else if height < pool.currentHeight()+maxPublishForesight { - // even if the peer have subscribe at the height before, in consideration of - // network/protocol robustness, still sent what we have again. - if blockState != nil { - if blockState.latestBlock != nil { - pool.sendMessages(src.ID(), &blockCommitResponseMessage{Block: blockState.latestBlock}) - } - if blockState.latestCommit != nil { - pool.sendMessages(src.ID(), &blockCommitResponseMessage{Commit: blockState.latestCommit}) - } - } - pool.Logger.Debug("handle peer subscribe block", "peer", src.ID(), "height", height) - if peers, exist := pool.subscriberPeerSets[height]; exist { - peers[src.ID()] = struct{}{} - } else { - pool.subscriberPeerSets[height] = map[p2p.ID]struct{}{src.ID(): {}} - } - } else { - // will not handle higher subscribe request - pool.sendMessages(src.ID(), &noBlockResponseMessage{height}) - return - } - } - return -} - -func (pool *BlockPool) handleUnSubscribeBlock(height int64, src p2p.Peer) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - if pool.st == Mute { - return - } - pool.Logger.Debug("handle peer unsubscribe block", "peer", src.ID(), "height", height) - if subscribers, exist := pool.subscriberPeerSets[height]; exist { - delete(subscribers, src.ID()) - } -} - -func (pool *BlockPool) handleBlockCommit(block *types.Block, commit *types.Commit, src p2p.Peer) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - if pool.st != Hot { - return - } - var height int64 - // have checked, block and commit can't both be nil - if block != nil { - height = block.Height - } else { - height = commit.Height() - } - if ps := pool.getPublisherAtHeight(height, src.ID()); ps != nil { - if block != nil { - ps.receiveBlock(block) - } - if commit != nil { - ps.receiveCommit(commit) - } - } else { - pool.Logger.Info("receive block/commit that is not expected", "peer", src.ID(), "height", height) - } -} - -func (pool *BlockPool) handleNoBlock(height int64, src p2p.Peer) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - if pool.st != Hot { - return - } - if ps := pool.getPublisherAtHeight(height, src.ID()); ps != nil { - ps.receiveNoBlock() - } else { - pool.Logger.Info("receive no block message that is not expected", "peer", src.ID(), "height", height) - } -} - -func (pool *BlockPool) handleBlockFromSelf(height int64, block *types.Block) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - bc, exist := pool.blockStates[height] - if !exist { - bc = pool.newBlockStateAtHeight(height) - } - ps, exist := bc.pubStates[selfId] - if !exist { - ps = NewPeerPublisherState(bc, selfId, pool.Logger, pool.blockTimeout, pool) - bc.pubStates[selfId] = ps - } - ps.receiveBlock(block) -} - -func (pool *BlockPool) handleCommitFromSelf(height int64, commit *types.Commit) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - bc, exist := pool.blockStates[height] - if !exist { - bc = pool.newBlockStateAtHeight(height) - } - ps, exist := bc.pubStates[selfId] - if !exist { - ps = NewPeerPublisherState(bc, selfId, pool.Logger, pool.blockTimeout, pool) - bc.pubStates[selfId] = ps - } - ps.receiveCommit(commit) -} - -// ------------------------------------------------- -func (pool *BlockPool) hotSyncRoutine() { - tryRepairTicker := time.NewTicker(tryRepairInterval) - defer tryRepairTicker.Stop() - pool.subscribeBlockInForesightExclusive() - for { - if !pool.IsRunning() { - break - } - select { - case bs := <-pool.blockStateSealCh: - pool.compensatePublish(bs) - pool.applyBlock(bs) - pool.recordMetrics(bs.block) - pool.incBlocksSynced() - pool.incCurrentHeight() - pool.trimStaleState() - // try subscribe more - pool.subscribeBlockInForesightExclusive() - pool.wakeupNextBlockState() - case ps := <-pool.publisherStateSealCh: - pool.updatePeerMetrics(ps) - if ps.broken { - pool.decayedPeers <- decayedPeer{ps.pid, ps.bs.height} - } else { - pool.updatePeerState(ps.pid) - } - case peer := <-pool.decayedPeers: - pool.tryReschedule(peer) - case <-tryRepairTicker.C: - pool.tryRepair() - case <-pool.switchCh: - pool.Logger.Info("stopping hotsync routine") - pool.switchWg.Done() - return - } - } -} - -func (pool *BlockPool) pubFromStoreRoutine() { - for { - select { - case <-pool.Quit(): - return - case subMsg := <-pool.pubFromStoreCh: - pool.sendBlockFromStore(subMsg.height, subMsg.src) - } - } -} - -func (pool *BlockPool) consensusSyncRoutine() { - blockSub, err := pool.eventBus.Subscribe(context.Background(), subscriber, types.EventQueryCompleteProposal, eventBusSubscribeCap) - if err != nil { - panic(err) - } - commitSub, err := pool.eventBus.Subscribe(context.Background(), subscriber, types.EventQueryMajorPrecommits, eventBusSubscribeCap) - if err != nil { - panic(err) - } - for { - if !pool.IsRunning() { - break - } - select { - case bs := <-pool.blockStateSealCh: - // there is no guarantee that this routine happens before consensus reactor start. - // should tolerate miss some blocks at first. - // so we use setCurrentHeight instead of incHeight. - pool.setCurrentHeight(bs.height) - pool.compensatePublish(bs) - pool.trimStaleState() - case <-pool.publisherStateSealCh: - // just drain the channel - case blockData := <-blockSub.Out(): - blockProposal := blockData.Data().(types.EventDataCompleteProposal) - block := blockProposal.Block - height := blockProposal.Height - pool.handleBlockFromSelf(height, &block) - case commitData := <-commitSub.Out(): - precommit := commitData.Data().(types.EventDataMajorPrecommits) - // if maj23 is zero, means will have another round. - if maj23, ok := precommit.Votes.TwoThirdsMajority(); ok && !maj23.IsZero() { - height := precommit.Height - commit := precommit.Votes.MakeCommit() - pool.handleCommitFromSelf(height, commit) - } - } - } - -} - -//------------------------------------- -func (pool *BlockPool) resetBlockStates() { - for height := pool.expectingHeight(); height <= pool.subscribedHeight; height++ { - if pubs := pool.getPublishersAtHeight(height); pubs != nil { - for _, ps := range pubs { - if ps.timeout != nil { - ps.timeout.Stop() - } - if !ps.sealed { - pool.sendMessages(ps.pid, &blockUnSubscribeMessage{Height: ps.bs.height}) - } - } - } - delete(pool.blockStates, height) - } -} - -func (pool *BlockPool) tryReschedule(peer decayedPeer) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - height := peer.fromHeight - if height == 0 { - height = pool.expectingHeight() - } - pool.Logger.Info("try reschedule for block", "from_height", height, "to_height", pool.subscribedHeight, "peer", peer.peerId) - for ; height <= pool.subscribedHeight; height++ { - if pubStates := pool.getPublishersAtHeight(height); pubStates != nil { - if ps, exist := pubStates[peer.peerId]; exist { - if ps.timeout != nil { - ps.timeout.Stop() - } - // if the peer is removed, try send unsubscribeMessage actually will do nothing. - pool.sendMessages(peer.peerId, &blockUnSubscribeMessage{Height: height}) - delete(pubStates, ps.pid) - // is is possible the current height blockstate is sealed, but current height is not update yet. - // if it is sealed, just skip - if !ps.bs.isSealed() && len(pubStates) == 0 { - // empty now, choose new peers for this height. - pool.subscribeBlockAtHeight(height, nil) - } - } - } - } -} - -func (pool *BlockPool) tryRepair() { - pool.mtx.Lock() - defer pool.mtx.Unlock() - // have't block any blocks yet. - if pool.currentHeight() == pool.subscribedHeight { - pool.subscribeBlockInForesight() - } else { - // reschedule may failed because of no peer available, need subscribe again during repair - for h := pool.expectingHeight(); h <= pool.subscribedHeight; h++ { - if pubStates := pool.getPublishersAtHeight(h); len(pubStates) == 0 { - pool.Logger.Info("try resubscribe for block", "height", h) - pool.subscribeBlockAtHeight(h, nil) - } - } - } -} - -func (pool *BlockPool) wakeupNextBlockState() { - pool.mtx.Lock() - defer pool.mtx.Unlock() - pool.Logger.Info("Wake next block", "height", pool.expectingHeight()) - if bs := pool.blockStates[pool.expectingHeight()]; bs != nil { - // new round, sent new start time - bs.startTime = time.Now() - if pubs := pool.getPublishersAtHeight(pool.expectingHeight()); pubs != nil { - for _, ps := range pubs { - ps.wakeup() - } - } - } -} - -func (pool *BlockPool) sendBlockFromStore(height int64, src p2p.Peer) { - // skip load block when the peer is already stopped. - if !src.IsRunning() { - return - } - pool.Logger.Debug("send block from store", "peer", src.ID(), "height", height) - block := pool.store.LoadBlock(height) - if block == nil { - pool.sendMessages(src.ID(), &noBlockResponseMessage{height}) - return - } - blockAfter := pool.store.LoadBlock(height + 1) - if blockAfter == nil { - pool.sendMessages(src.ID(), &noBlockResponseMessage{height}) - return - } - pool.sendMessages(src.ID(), &blockCommitResponseMessage{Block: block, Commit: blockAfter.LastCommit}) -} - -// applyBlock takes most of time, make thin lock in updateState. -func (pool *BlockPool) applyBlock(bs *blockState) { - pool.Logger.Info("Apply block", "height", bs.block.Height) - blockParts := bs.block.MakePartSet(types.BlockPartSizeBytes) - pool.store.SaveBlock(bs.block, blockParts, bs.commit) - // get the hash without persisting the state - newState, err := pool.blockExec.ApplyBlock(pool.state, *bs.blockId, bs.block) - if err != nil { - panic(fmt.Sprintf("Failed to process committed blockChainMessage (%d:%X): %v", bs.block.Height, bs.block.Hash(), err)) - } - pool.updateState(newState) -} - -func (pool *BlockPool) updateState(state st.State) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - pool.state = state -} - -// the subscribe strategy: -// -// one candidate: -// 1. if `subscribedHeight-currentHeight > maxSubscribeForesight/2`, means have subscribe many blocks now, do nothing -// 2. else [subscribedHeight + 1, currentHeight+maxSubscribeForesight] subscribe request will be send. -// -// more than one candidates: only block at `subscribedHeight + 1` will be subscribed. -func (pool *BlockPool) subscribeBlockInForesight() { - peers := pool.candidatePool.PickCandidates() - if len(peers) == 0 { - pool.Logger.Error("no peers is available", "height", pool.currentHeight()) - return - } - var fromHeight, toHeight int64 - if len(peers) == 1 { - peer := peers[0] - if pool.subscribedHeight-pool.currentHeight() > maxSubscribeForesight/2 { - return - } else { - fromHeight = pool.subscribedHeight + 1 - toHeight = pool.currentHeight() + maxSubscribeForesight - } - for h := fromHeight; h <= toHeight; h++ { - bc := pool.newBlockStateAtHeight(h) - bc.pubStates[*peer] = NewPeerPublisherState(bc, *peer, pool.Logger, pool.blockTimeout, pool) - } - pool.sendMessages(*peer, &blockSubscribeMessage{fromHeight, toHeight}) - pool.subscribedHeight = toHeight - return - } else if len(peers) > 1 { - if pool.subscribedHeight+1 <= pool.currentHeight()+maxSubscribeForesight { - height := pool.subscribedHeight + 1 - pool.subscribeBlockAtHeight(height, peers) - pool.subscribedHeight = height - } - return - } -} - -func (pool *BlockPool) subscribeBlockInForesightExclusive() { - pool.mtx.Lock() - defer pool.mtx.Unlock() - pool.subscribeBlockInForesight() - -} - -func (pool *BlockPool) newBlockStateAtHeight(height int64) *blockState { - newBc := NewBlockState(height, pool.Logger, pool) - pool.blockStates[height] = newBc - return newBc -} - -func (pool *BlockPool) subscribeBlockAtHeight(height int64, peers []*p2p.ID) { - if peers == nil { - peers = pool.candidatePool.PickCandidates() - } - if len(peers) == 0 { - pool.Logger.Error("no peers is available", "height", pool.currentHeight()) - return - } - bc := pool.newBlockStateAtHeight(height) - for _, peer := range peers { - ps := NewPeerPublisherState(bc, *peer, pool.Logger, pool.blockTimeout, pool) - bc.pubStates[*peer] = ps - pool.sendMessages(*peer, &blockSubscribeMessage{height, height}) - } -} - -// May publish order and commit in incorrect order, try correct it once we know the true block and commit. -// It is expensive to maintain all received blocks before receiving a -// valid commit, this is a trade off between complexity, memory and network. -func (pool *BlockPool) compensatePublish(bs *blockState) { - pool.mtx.Lock() - defer pool.mtx.Unlock() - if !bytes.Equal(bs.block.Hash(), bs.latestBlock.Hash()) { - bs.pool.publishBlock(bs.height, bs.block) - } - if !bytes.Equal(bs.commit.Hash(), bs.latestCommit.Hash()) { - bs.pool.publishCommit(bs.height, bs.commit) - } - delete(bs.pool.subscriberPeerSets, bs.height) -} - -func (pool *BlockPool) publishBlock(height int64, block *types.Block) { - if subscribers := pool.subscriberPeerSets[height]; len(subscribers) > 0 { - for p := range subscribers { - pool.sendMessages(p, &blockCommitResponseMessage{Block: block}) - } - } -} - -func (pool *BlockPool) publishCommit(height int64, commit *types.Commit) { - if subscribers := pool.subscriberPeerSets[height]; len(subscribers) > 0 { - for p := range subscribers { - pool.sendMessages(p, &blockCommitResponseMessage{Commit: commit}) - } - } -} - -func (pool *BlockPool) trimStaleState() { - pool.mtx.Lock() - defer pool.mtx.Unlock() - trimHeight := pool.currentHeight() - maxCachedSealedBlock - if trimHeight > 0 { - if pubStates := pool.getPublishersAtHeight(trimHeight); pubStates != nil { - for _, ps := range pubStates { - if pool.st == Hot { - ps.tryExpire() - } - } - } - delete(pool.blockStates, trimHeight) - } -} - -func (pool *BlockPool) updatePeerState(pid p2p.ID) { - if pool.candidatePool.swh == nil || pool.candidatePool.swh.Peers() == nil { - return - } - if p := pool.candidatePool.swh.Peers().Get(pid); p != nil { - peerState, ok := p.Get(types.PeerStateKey).(peerState) - if !ok { - // Peer does not have a state yet. It is set in the consensus reactor. - return - } - peerState.SetHeight(pool.expectingHeight()) - } -} - -func (pool *BlockPool) getPublishersAtHeight(height int64) map[p2p.ID]*publisherState { - if bs, exist := pool.blockStates[height]; exist { - return bs.pubStates - } - return nil -} - -func (pool *BlockPool) getPublisherAtHeight(height int64, pid p2p.ID) *publisherState { - if pubs := pool.getPublishersAtHeight(height); pubs != nil { - return pubs[pid] - } - return nil -} - -// where use currentHeight have already locked -func (pool *BlockPool) currentHeight() int64 { - return pool.height -} - -func (pool *BlockPool) incCurrentHeight() { - pool.mtx.Lock() - pool.height++ - pool.mtx.Unlock() -} - -func (pool *BlockPool) setCurrentHeight(height int64) { - pool.mtx.Lock() - pool.height = height - pool.mtx.Unlock() -} - -// most of the usage have locked. Only newBlockStateAtHeight do not, but the usage can't -// currently excuted with incCurrentHeight, it is safe. -func (pool *BlockPool) expectingHeight() int64 { - return pool.height + 1 -} - -func (pool *BlockPool) incBlocksSynced() int32 { - return atomic.AddInt32(&pool.blocksSynced, 1) -} - -func (pool *BlockPool) getBlockSynced() int32 { - return atomic.LoadInt32(&pool.blocksSynced) -} - -func (pool *BlockPool) getSyncPattern() SyncPattern { - pool.mtx.Lock() - defer pool.mtx.Unlock() - return pool.st -} - -func (pool *BlockPool) verifyCommit(blockID types.BlockID, commit *types.Commit) error { - return pool.state.Validators.VerifyCommit(pool.state.ChainID, blockID, pool.currentHeight()+1, commit) -} - -func (pool *BlockPool) sendMessages(pid p2p.ID, msgs ...BlockchainMessage) { - for _, msg := range msgs { - select { - case pool.sendCh <- Message{msg, pid}: - default: - pool.Logger.Error("Failed to send commit/blockChainMessage since messagesCh is full", "peer", pid) - } - } -} - -func (pool *BlockPool) updatePeerMetrics(ps *publisherState) { - var et eventType - var dur int64 - if ps.broken { - et = Bad - } else { - et = Good - dur = time.Now().Sub(ps.bs.startTime).Nanoseconds() - // this should not happened, but defend it. - if dur <= 0 { - dur = 1 - } - } - select { - case ps.pool.sampleStream <- metricsEvent{et, ps.pid, dur}: - default: - ps.logger.Error("failed to send good sample event", "peer", ps.pid, "height", ps.bs.height) - } - return -} - -func (pool *BlockPool) recordMetrics(block *types.Block) { - height := block.Height - if height > 1 { - var lastBlockTime time.Time - if lastBs, exist := pool.blockStates[height-1]; exist { - lastBlockTime = lastBs.block.Time - } else { - lastBlockTime = pool.store.LoadBlockMeta(height - 1).Header.Time - } - pool.metrics.BlockIntervalSeconds.Set( - block.Time.Sub(lastBlockTime).Seconds(), - ) - } - pool.metrics.NumTxs.Set(float64(block.NumTxs)) - pool.metrics.BlockSizeBytes.Set(float64(block.Size())) - pool.metrics.TotalTxs.Set(float64(block.TotalTxs)) - pool.metrics.CommittedHeight.Set(float64(height)) - pool.metrics.Height.Set(float64(height + 1)) -} - -//---------------------------------------- -// blockState track the response of multi peers about block/commit at specified -// height that blockPool subscribed. -type blockState struct { - //--- ops fields - mux sync.Mutex - logger log.Logger - startTime time.Time - - pool *BlockPool - sealed bool - - //--- data fields - height int64 - commit *types.Commit - block *types.Block - blockId *types.BlockID - - // recently received commit and blockChainMessage - latestCommit *types.Commit - latestBlock *types.Block - - pubStates map[p2p.ID]*publisherState -} - -func NewBlockState(height int64, logger log.Logger, pool *BlockPool) *blockState { - bc := &blockState{ - height: height, - logger: logger, - pool: pool, - pubStates: make(map[p2p.ID]*publisherState, 0), - } - if pool.expectingHeight() == height { - bc.startTime = time.Now() - } - return bc -} - -func (bs *blockState) setLatestBlock(block *types.Block) { - bs.mux.Lock() - defer bs.mux.Unlock() - if bs.sealed { - return - } - if bs.latestBlock == nil || !bytes.Equal(bs.latestBlock.Hash(), block.Hash()) { - bs.latestBlock = block - // notice blockChainMessage state change - bs.pool.publishBlock(bs.height, block) - } -} - -func (bs *blockState) setLatestCommit(commit *types.Commit) { - bs.mux.Lock() - defer bs.mux.Unlock() - if bs.sealed { - return - } - if bs.latestCommit == nil || !bytes.Equal(bs.latestCommit.Hash(), commit.Hash()) { - bs.latestCommit = commit - // notice blockChainMessage state change - bs.pool.publishCommit(bs.height, commit) - } -} - -func (bs *blockState) seal() { - bs.mux.Lock() - defer bs.mux.Unlock() - if bs.sealed == true { - return - } - bs.sealed = true - bs.logger.Debug("sealing blockChainMessage commit", "height", bs.height) - bs.pool.blockStateSealCh <- bs -} - -func (bs *blockState) isSealed() bool { - bs.mux.Lock() - defer bs.mux.Unlock() - return bs.sealed -} - -//---------------------------------------- -// publisherState is used to track the continuous response of a peer about block/commit at specified -// height that blockpool subscribed. The peer plays as a publisher. -type publisherState struct { - mux sync.Mutex - logger log.Logger - timeoutDur time.Duration - - bs *blockState - pool *BlockPool - - timeout *time.Timer - isWake bool - - broken bool - sealed bool - - pid p2p.ID - commit *types.Commit - block *types.Block -} - -func NewPeerPublisherState(bc *blockState, pid p2p.ID, logger log.Logger, timeoutDur time.Duration, pool *BlockPool) *publisherState { - ps := &publisherState{ - logger: logger, - pid: pid, - bs: bc, - pool: pool, - timeoutDur: timeoutDur, - } - if pool.expectingHeight() == bc.height || pid == selfId { - if pool.st == Hot { - ps.startTimer() - } - ps.isWake = true - } - return ps -} - -func (ps *publisherState) wakeup() { - ps.mux.Lock() - defer ps.mux.Unlock() - ps.startTimer() - ps.isWake = true - ps.trySeal() -} - -// if the ps still not trig timeout, but is going to trim, -// consider it as bad peer, and update metrics by itself. -func (ps *publisherState) tryExpire() { - ps.mux.Lock() - defer ps.mux.Unlock() - if !ps.sealed { - // stop the timer in case it leaks - if ps.timeout != nil { - ps.timeout.Stop() - } - ps.broken = true - ps.pool.updatePeerMetrics(ps) - } -} - -func (ps *publisherState) onTimeout() { - ps.mux.Lock() - defer ps.mux.Unlock() - if ps.sealed { - return - } - ps.broken = true - ps.seal() -} - -func (ps *publisherState) receiveBlock(block *types.Block) { - ps.mux.Lock() - defer ps.mux.Unlock() - if ps.sealed { - ps.logger.Debug("received blockChainMessage that is already sealed", "peer", ps.pid, "height", ps.bs.height) - return - } - ps.block = block - ps.bs.setLatestBlock(block) - ps.trySeal() -} - -func (ps *publisherState) receiveNoBlock() { - ps.mux.Lock() - defer ps.mux.Unlock() - if ps.sealed { - return - } - ps.broken = true - ps.seal() -} - -func (ps *publisherState) receiveCommit(commit *types.Commit) { - ps.mux.Lock() - defer ps.mux.Unlock() - if ps.sealed { - ps.logger.Debug("received commit that is already sealed", "peer", ps.pid, "height", ps.bs.height) - return - } - ps.commit = commit - ps.bs.setLatestCommit(commit) - ps.trySeal() -} - -func (ps *publisherState) trySeal() { - if ps.bs.isSealed() { - ps.tryLaterSeal() - } else { - ps.tryFirstSeal() - } -} - -func (ps *publisherState) tryLaterSeal() { - if ps.block == nil || ps.commit == nil { - return - } - blockId := makeBlockID(ps.block) - // For a later sealed request, just choose to compare the the blockChainMessage id to - // decide whether it is a good peer. Fully verification is costly and - // no strong need to do so since we only used to judge if the peer is good or not. - if blockId.Equals(ps.commit.BlockID) && blockId.Equals(*ps.bs.blockId) { - ps.seal() - } else { - // maybe blockChainMessage is late, won't seal. - ps.logger.Info("received inconsistent blockChainMessage/commit", "peer", ps.pid, "height", ps.bs.height) - } -} - -func (ps *publisherState) tryFirstSeal() { - if ps.block == nil || ps.commit == nil { - return - } - // not now - if !ps.isWake { - return - } - blockId := makeBlockID(ps.block) - if ps.pid == selfId { - if !blockId.Equals(ps.commit.BlockID) { - return - } - } else if err := ps.pool.verifyCommit(*blockId, ps.commit); err != nil { - // maybe blockChainMessage is late, won't seal. - ps.logger.Info("received inconsistent blockChainMessage/commit", "peer", ps.pid, ps.bs.height) - return - } - ps.bs.commit = ps.commit - ps.bs.block = ps.block - ps.bs.blockId = blockId - ps.seal() - ps.bs.seal() - return -} - -// only start timer when blockpool reach at this height. -func (ps *publisherState) startTimer() { - if ps.timeout != nil { - ps.timeout.Reset(ps.timeoutDur) - } else { - ps.timeout = time.AfterFunc(ps.timeoutDur, ps.onTimeout) - } -} - -func (ps *publisherState) seal() { - if ps.timeout != nil { - ps.timeout.Stop() - } - ps.sealed = true - ps.logger.Debug("publisher state sealing", "peer", ps.pid, "height", ps.bs.height, "broken", ps.broken) - ps.pool.publisherStateSealCh <- ps -} - -// ----------------------- -type Message struct { - blockChainMessage BlockchainMessage - peerId p2p.ID -} - -type decayedPeer struct { - peerId p2p.ID - fromHeight int64 -} - -type subFromStoreMsg struct { - src p2p.Peer - height int64 -} - -func makeBlockID(block *types.Block) *types.BlockID { - blockParts := block.MakePartSet(types.BlockPartSizeBytes) - blockPartsHeader := blockParts.Header() - return &types.BlockID{Hash: block.Hash(), PartsHeader: blockPartsHeader} -} - -type peerState interface { - SetHeight(height int64) -} diff --git a/blockchain/hot/pool_test.go b/blockchain/hot/pool_test.go deleted file mode 100644 index a648b4f3d..000000000 --- a/blockchain/hot/pool_test.go +++ /dev/null @@ -1,604 +0,0 @@ -package hot - -import ( - "os" - "sort" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/blockchain" - cfg "github.com/tendermint/tendermint/config" - cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p/mock" - "github.com/tendermint/tendermint/proxy" - sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" - tmtime "github.com/tendermint/tendermint/types/time" -) - -var ( - config *cfg.Config -) - -func TestBlockPoolHotSyncBasic(t *testing.T) { - // prepare - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - messageChan := poolPair.messageQueue - pool.Start() - defer pool.Stop() - testPeer := mock.NewPeer(nil) - pool.AddPeer(testPeer) - - // consume subscribe message - <-messageChan - - //handle subscribe message - subscriber := mock.NewPeer(nil) - totalTestBlock := int64(10) - pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) - - for i := int64(0); i < totalTestBlock; i++ { - height := pool.currentHeight() - block, commit, _ := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) - expectedBlockMess1 := blockCommitResponseMessage{Block: block} - expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} - pool.handleBlockCommit(block, commit, testPeer) - var receive1, receive2 bool - for m := range messageChan { - if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { - if !receive1 { - assert.Equal(t, *bm, expectedBlockMess1) - receive1 = true - } else if !receive2 { - assert.Equal(t, *bm, expectedBlockMess2) - receive2 = true - } - if receive1 && receive2 { - break - } - } - } - for { - if height+1 != pool.currentHeight() { - time.Sleep(10 * time.Millisecond) - } else { - break - } - } - } - assert.Equal(t, int64(pool.blocksSynced), totalTestBlock) -} - -func TestBlockPoolHotSyncTimeout(t *testing.T) { - // prepare - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - messageChan := poolPair.messageQueue - pool.Start() - defer pool.Stop() - testPeer := mock.NewPeer(nil) - pool.AddPeer(testPeer) - - // send subscribe message for several times - expectedMessages := make([]BlockchainMessage, 0, 1+2*maxSubscribeForesight) - expectedMessages = append(expectedMessages, &blockSubscribeMessage{FromHeight: initBlockHeight + 1, ToHeight: initBlockHeight + maxSubscribeForesight}) - for j := 0; j < 3; j++ { - for i := int64(0); i < maxSubscribeForesight; i++ { - expectedMessages = append(expectedMessages, &blockUnSubscribeMessage{Height: initBlockHeight + i + 1}) - expectedMessages = append(expectedMessages, &blockSubscribeMessage{FromHeight: initBlockHeight + i + 1, ToHeight: initBlockHeight + i + 1}) - } - } - for _, expect := range expectedMessages { - m := <-messageChan - assert.Equal(t, m.blockChainMessage, expect) - } -} - -func TestBlockPoolHotSyncSubscribePastBlock(t *testing.T) { - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(100) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - messageChan := poolPair.messageQueue - pool.Start() - defer pool.Stop() - - subscriber := mock.NewPeer(nil) - pool.handleSubscribeBlock(1, initBlockHeight, subscriber) - - expectBlockMessage := make([]blockCommitResponseMessage, initBlockHeight-1) - for i := int64(1); i < int64(initBlockHeight); i++ { - first := pool.store.LoadBlock(i) - second := pool.store.LoadBlock(i + 1) - expectBlockMessage[i-1].Block = first - expectBlockMessage[i-1].Commit = second.LastCommit - } - var index int - for m := range messageChan { - if blockMes, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { - assert.Equal(t, *blockMes, expectBlockMessage[index]) - index++ - if int64(index) >= initBlockHeight-1 { - break - } - } - } -} - -func TestBlockPoolHotSyncSubscribeTooFarBlock(t *testing.T) { - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - messageChan := poolPair.messageQueue - pool.Start() - defer pool.Stop() - - subscriber := mock.NewPeer(nil) - pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+2*maxPublishForesight, subscriber) - for m := range messageChan { - if noBlock, ok := m.blockChainMessage.(*noBlockResponseMessage); ok { - assert.Equal(t, noBlock.Height, initBlockHeight+maxPublishForesight) - return - } - } -} - -func TestBlockPoolHotSyncUnSubscribe(t *testing.T) { - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - pool.Start() - defer pool.Stop() - - subscriber := mock.NewPeer(nil) - totalTestBlock := int64(10) - pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) - for i := initBlockHeight + 1; i <= initBlockHeight+totalTestBlock; i++ { - pool.handleUnSubscribeBlock(i, subscriber) - assert.Zero(t, len(pool.subscriberPeerSets[i])) - } -} - -func TestBlockPoolHotSyncRemovePeer(t *testing.T) { - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - messageChan := poolPair.messageQueue - pool.Start() - defer pool.Stop() - - testPeer := mock.NewPeer(nil) - pool.AddPeer(testPeer) - // wait for peer have a try - time.Sleep(2 * tryRepairInterval) - pool.RemovePeer(testPeer) - // drain subscribe message - <-messageChan - for i := int64(0); i < maxSubscribeForesight; i++ { - m := <-messageChan - noBlock, ok := m.blockChainMessage.(*blockUnSubscribeMessage) - assert.True(t, ok) - assert.Equal(t, noBlock.Height, initBlockHeight+i+1) - } -} - -func TestBlockPoolConsensusSyncBasic(t *testing.T) { - eventBus := types.NewEventBus() - err := eventBus.Start() - require.NoError(t, err) - defer eventBus.Stop() - - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - pool.st = Consensus - pool.setEventBus(eventBus) - messageChan := poolPair.messageQueue - pool.Start() - defer pool.Stop() - //handle subscribe message - subscriber := mock.NewPeer(nil) - totalTestBlock := int64(10) - pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) - - for i := int64(0); i < totalTestBlock; i++ { - height := pool.currentHeight() - block, commit, vs := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) - expectedBlockMess1 := blockCommitResponseMessage{Block: block} - //generate hash - commit.Hash() - expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} - err := eventBus.PublishEventCompleteProposal(types.EventDataCompleteProposal{Height: block.Height, Block: *block}) - assert.NoError(t, err) - err = eventBus.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{Height: block.Height, Votes: *vs}) - assert.NoError(t, err) - pool.applyBlock(&blockState{ - block: block, - commit: commit, - blockId: makeBlockID(block), - }) - var receive1, receive2 bool - for m := range messageChan { - if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { - if !receive1 { - assert.Equal(t, *bm, expectedBlockMess1) - receive1 = true - } else if !receive2 { - assert.Equal(t, *bm, expectedBlockMess2) - receive2 = true - } - if receive1 && receive2 { - break - } - } - } - for { - if height+1 != pool.currentHeight() { - time.Sleep(10 * time.Millisecond) - } else { - break - } - } - } -} - -func TestBlockPoolSubscribeFromCache(t *testing.T) { - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - messageChan := poolPair.messageQueue - pool.Start() - defer pool.Stop() - testPeer := mock.NewPeer(nil) - pool.AddPeer(testPeer) - - // consume subscribe message - <-messageChan - - totalTestBlock := int64(10) - - expectedBlockMess := make([]blockCommitResponseMessage, 0, totalTestBlock) - for i := int64(0); i < totalTestBlock; i++ { - height := pool.currentHeight() - block, commit, _ := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) - expectedBlockMess = append(expectedBlockMess, blockCommitResponseMessage{Block: block, Commit: commit}) - pool.handleBlockCommit(block, commit, testPeer) - - for { - if height+1 != pool.currentHeight() { - time.Sleep(10 * time.Millisecond) - } else { - break - } - } - } - //handle subscribe message - subscriber := mock.NewPeer(nil) - pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) - for i := int64(0); i < totalTestBlock; i++ { - for m := range messageChan { - if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { - assert.Equal(t, *bm, expectedBlockMess[i]) - break - } - } - } -} - -func TestBlockPoolSwitch(t *testing.T) { - config = cfg.ResetTestRoot(t.Name()) - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - initBlockHeight := int64(10) - poolPair := newBlockchainPoolPair(log.TestingLogger(), genDoc, privVals, initBlockHeight) - - pool := poolPair.pool - messageChan := poolPair.messageQueue - pool.st = Mute - pool.Start() - defer pool.Stop() - testPeer := mock.NewPeer(nil) - pool.AddPeer(testPeer) - - time.Sleep(10 * time.Millisecond) - pool.SwitchToHotSync(pool.state, 100) - // consume subscribe message - <-messageChan - - //handle subscribe message - subscriber := mock.NewPeer(nil) - totalTestBlock := int64(10) - pool.handleSubscribeBlock(initBlockHeight+1, initBlockHeight+totalTestBlock, subscriber) - - for i := int64(0); i < totalTestBlock; i++ { - height := pool.currentHeight() - block, commit, _ := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) - expectedBlockMess1 := blockCommitResponseMessage{Block: block} - expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} - pool.handleBlockCommit(block, commit, testPeer) - var receive1, receive2 bool - for m := range messageChan { - if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { - if !receive1 { - assert.Equal(t, *bm, expectedBlockMess1) - receive1 = true - } else if !receive2 { - assert.Equal(t, *bm, expectedBlockMess2) - receive2 = true - } - if receive1 && receive2 { - break - } - } - } - for { - if height+1 != pool.currentHeight() { - time.Sleep(10 * time.Millisecond) - } else { - break - } - } - } - assert.Equal(t, int64(pool.blocksSynced), totalTestBlock+100) - eventBus := types.NewEventBus() - err := eventBus.Start() - require.NoError(t, err) - defer eventBus.Stop() - pool.setEventBus(eventBus) - - pool.SwitchToConsensusSync(nil) - time.Sleep(10 * time.Millisecond) - subscriber = mock.NewPeer(nil) - totalTestBlock = int64(10) - pool.handleSubscribeBlock(pool.currentHeight()+1, pool.currentHeight()+totalTestBlock, subscriber) - - for i := int64(0); i < totalTestBlock; i++ { - height := pool.currentHeight() - block, commit, vs := nextBlock(poolPair.pool.state, poolPair.pool.store, poolPair.pool.blockExec, poolPair.privVals[0]) - expectedBlockMess1 := blockCommitResponseMessage{Block: block} - //generate hash - commit.Hash() - expectedBlockMess2 := blockCommitResponseMessage{Commit: commit} - err := eventBus.PublishEventCompleteProposal(types.EventDataCompleteProposal{Height: block.Height, Block: *block}) - assert.NoError(t, err) - err = eventBus.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{Height: block.Height, Votes: *vs}) - assert.NoError(t, err) - pool.applyBlock(&blockState{ - block: block, - commit: commit, - blockId: makeBlockID(block), - }) - var receive1, receive2 bool - for m := range messageChan { - if bm, ok := m.blockChainMessage.(*blockCommitResponseMessage); ok { - if !receive1 { - eq := assert.Equal(t, *bm, expectedBlockMess1) - receive1 = true - if !eq { - return - } - } else if !receive2 { - eq := assert.Equal(t, *bm, expectedBlockMess2) - receive2 = true - receive1 = true - if !eq { - return - } - } - if receive1 && receive2 { - break - } - } - } - for { - if height+1 != pool.currentHeight() { - time.Sleep(10 * time.Millisecond) - } else { - break - } - } - } - -} - -func randGenesisDoc(numValidators int, randPower bool, minPower int64) (*types.GenesisDoc, []types.PrivValidator) { - validators := make([]types.GenesisValidator, numValidators) - privValidators := make([]types.PrivValidator, numValidators) - for i := 0; i < numValidators; i++ { - val, privVal := types.RandValidator(randPower, minPower) - validators[i] = types.GenesisValidator{ - PubKey: val.PubKey, - Power: val.VotingPower, - } - privValidators[i] = privVal - } - sort.Sort(types.PrivValidatorsByAddress(privValidators)) - - return &types.GenesisDoc{ - GenesisTime: tmtime.Now(), - ChainID: config.ChainID(), - Validators: validators, - }, privValidators -} - -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < 10; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) - return block -} - -func makeVote(header *types.Header, blockID types.BlockID, valset *types.ValidatorSet, privVal types.PrivValidator) *types.Vote { - addr := privVal.GetPubKey().Address() - idx, _ := valset.GetByAddress(addr) - vote := &types.Vote{ - ValidatorAddress: addr, - ValidatorIndex: idx, - Height: header.Height, - Round: 1, - Timestamp: tmtime.Now(), - Type: types.PrecommitType, - BlockID: blockID, - } - - privVal.SignVote(header.ChainID, vote) - - return vote -} - -func newBlockchainPoolPair(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64) BlockPoolPair { - if len(privVals) != 1 { - panic("only support one validator") - } - app := &testApp{} - cc := proxy.NewLocalClientCreator(app) - proxyApp := proxy.NewAppConns(cc) - err := proxyApp.Start() - if err != nil { - panic(cmn.ErrorWrap(err, "error start app")) - } - blockDB := dbm.NewMemDB() - stateDB := dbm.NewMemDB() - blockStore := blockchain.NewBlockStore(blockDB) - state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) - if err != nil { - panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) - } - - // Make the BlockPool itself. - // NOTE we have to create and commit the blocks first because - // pool.height is determined from the store. - blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), proxyApp.Consensus(), - sm.MockMempool{}, sm.MockEvidencePool{}, true) - // let's add some blocks in - for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { - lastCommit := types.NewCommit(types.BlockID{}, nil) - if blockHeight > 1 { - lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) - lastBlock := blockStore.LoadBlock(blockHeight - 1) - - vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() - lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) - } - - thisBlock := makeBlock(blockHeight, state, lastCommit) - - thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) - blockID := types.BlockID{thisBlock.Hash(), thisParts.Header()} - - state, err = blockExec.ApplyBlock(state, blockID, thisBlock) - if err != nil { - panic(cmn.ErrorWrap(err, "error apply block")) - } - - blockStore.SaveBlock(thisBlock, thisParts, lastCommit) - } - messagesCh := make(chan Message, messageQueueSize) - bcPool := NewBlockPool(blockStore, blockExec, state.Copy(), messagesCh, Hot, 2*time.Second) - bcPool.SetLogger(logger.With("module", "blockpool")) - - return BlockPoolPair{bcPool, proxyApp, messagesCh, privVals} -} - -type BlockPoolPair struct { - pool *BlockPool - app proxy.AppConns - messageQueue chan Message - privVals []types.PrivValidator -} - -func nextBlock(state sm.State, blockStore *blockchain.BlockStore, blockExec *sm.BlockExecutor, pri types.PrivValidator) (*types.Block, *types.Commit, *types.VoteSet) { - height := blockStore.Height() - lastBlockMeta := blockStore.LoadBlockMeta(height) - lastBlock := blockStore.LoadBlock(height) - vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, pri).CommitSig() - lastCommit := types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) - thisBlock := makeBlock(height+1, state, lastCommit) - - thisBlockId := makeBlockID(thisBlock) - thisVote := makeVote(&thisBlock.Header, *thisBlockId, state.Validators, pri) - thisCommitSig := thisVote.CommitSig() - thisCommit := types.NewCommit(*makeBlockID(thisBlock), []*types.CommitSig{thisCommitSig}) - thisVoteSet := types.NewVoteSet(thisBlock.ChainID, thisBlock.Height, 1, types.PrecommitType, state.Validators) - thisVoteSet.AddVote(thisVote) - return thisBlock, thisCommit, thisVoteSet -} - -type testApp struct { - abci.BaseApplication -} - -var _ abci.Application = (*testApp)(nil) - -func (app *testApp) Info(req abci.RequestInfo) (resInfo abci.ResponseInfo) { - return abci.ResponseInfo{} -} - -func (app *testApp) BeginBlock(req abci.RequestBeginBlock) abci.ResponseBeginBlock { - return abci.ResponseBeginBlock{} -} - -func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { - return abci.ResponseEndBlock{} -} - -func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { - return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} -} - -func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { - return abci.ResponseCheckTx{} -} - -func (app *testApp) Commit() abci.ResponseCommit { - return abci.ResponseCommit{} -} - -func (app *testApp) Query(reqQuery abci.RequestQuery) (resQuery abci.ResponseQuery) { - return -} diff --git a/blockchain/hot/reactor.go b/blockchain/hot/reactor.go deleted file mode 100644 index 6643e0ee0..000000000 --- a/blockchain/hot/reactor.go +++ /dev/null @@ -1,365 +0,0 @@ -package hot - -import ( - "errors" - "fmt" - "reflect" - "time" - - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/blockchain" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -const ( - // HotBlockchainChannel is a channel for blocks/commits and status updates under hot sync mode. - HotBlockchainChannel = byte(0x41) - - messageQueueSize = 1000 - - switchToConsensusIntervalSeconds = 1 - - maxSubscribeBlocks = 50 - - // NOTE: keep up to date with blockCommitResponseMessage - blockCommitResponseMessagePrefixSize = 4 - blockCommitMessageFieldKeySize = 2 - // TODO, the size of commit is dynamic, assume no more than 1M. - commitSizeBytes = 1024 * 1024 - maxMsgSize = types.MaxBlockSizeBytes + - blockCommitResponseMessagePrefixSize + - blockCommitMessageFieldKeySize + - commitSizeBytes -) - -type consensusReactor interface { - // for when we switch from hotBlockchain reactor and hot sync to - // the consensus machine - SwitchToConsensus(sm.State, int) -} - -// BlockchainReactor handles low latency catchup when there is small lag. -type BlockchainReactor struct { - p2p.BaseReactor - - pool *BlockPool - privValidator types.PrivValidator - - messagesCh <-chan Message -} - -type BlockChainOption func(*BlockchainReactor) - -// NewBlockChainReactor returns new reactor instance. -func NewBlockChainReactor(state sm.State, blockExec *sm.BlockExecutor, store *blockchain.BlockStore, hotSync, fastSync bool, blockTimeout time.Duration, options ...BlockChainOption) *BlockchainReactor { - - if state.LastBlockHeight != store.Height() { - panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, - store.Height())) - } - - messagesCh := make(chan Message, messageQueueSize) - var st SyncPattern - if fastSync { - st = Mute - } else if hotSync { - st = Hot - } else { - st = Consensus - } - pool := NewBlockPool(store, blockExec, state, messagesCh, st, blockTimeout) - - hbcR := &BlockchainReactor{ - messagesCh: messagesCh, - pool: pool, - } - - for _, option := range options { - option(hbcR) - } - - hbcR.BaseReactor = *p2p.NewBaseReactor("HotSyncBlockChainReactor", hbcR) - return hbcR -} - -// WithMetrics sets the metrics. -func WithMetrics(metrics *Metrics) BlockChainOption { - return func(hbcR *BlockchainReactor) { hbcR.pool.setMetrics(metrics) } -} - -// WithMetrics sets the metrics. -func WithEventBus(eventBs *types.EventBus) BlockChainOption { - return func(hbcR *BlockchainReactor) { hbcR.pool.setEventBus(eventBs) } -} - -func (hbcR *BlockchainReactor) SetPrivValidator(priv types.PrivValidator) { - hbcR.privValidator = priv -} - -// SetLogger implements cmn.Service by setting the logger on reactor and pool. -func (hbcR *BlockchainReactor) SetLogger(l log.Logger) { - hbcR.BaseService.Logger = l - hbcR.pool.SetLogger(l) -} - -// OnStart implements cmn.Service. -func (hbcR *BlockchainReactor) OnStart() error { - err := hbcR.pool.Start() - if err != nil { - return err - } - go hbcR.poolRoutine() - go hbcR.switchRoutine() - return nil -} - -// OnStop implements cmn.Service. -func (hbcR *BlockchainReactor) OnStop() { - hbcR.pool.Stop() -} - -// GetChannels implements Reactor -func (hbcR *BlockchainReactor) GetChannels() []*p2p.ChannelDescriptor { - return []*p2p.ChannelDescriptor{ - { - ID: HotBlockchainChannel, - Priority: 10, - SendQueueCapacity: 1000, - RecvBufferCapacity: 50 * 4096, - RecvMessageCapacity: maxMsgSize, - }, - } -} - -func (hbcR *BlockchainReactor) AddPeer(peer p2p.Peer) { - hbcR.pool.AddPeer(peer) -} - -func (hbcR *BlockchainReactor) RemovePeer(peer p2p.Peer, reason interface{}) { - hbcR.pool.RemovePeer(peer) -} - -// Receive implements Reactor by handling 5 types of messages (look below). -func (hbcR *BlockchainReactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := decodeMsg(msgBytes) - if err != nil { - hbcR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) - hbcR.Switch.StopPeerForError(src, err) - return - } - - if err = msg.ValidateBasic(); err != nil { - hbcR.Logger.Error("Peer sent us invalid msg", "peer", src, "msg", msg, "err", err) - hbcR.Switch.StopPeerForError(src, err) - return - } - switch msg := msg.(type) { - case *blockSubscribeMessage: - hbcR.Logger.Debug("receive blockSubscribeMessage from peer", "peer", src.ID(), "from_height", msg.FromHeight, "to_height", msg.ToHeight) - hbcR.pool.handleSubscribeBlock(msg.FromHeight, msg.ToHeight, src) - case *blockCommitResponseMessage: - hbcR.Logger.Debug("receive blockCommitResponseMessage from peer", "peer", src.ID(), "height", msg.Height()) - hbcR.pool.handleBlockCommit(msg.Block, msg.Commit, src) - case *noBlockResponseMessage: - hbcR.Logger.Debug("receive noBlockResponseMessage from peer", "peer", src.ID(), "height", msg.Height) - hbcR.pool.handleNoBlock(msg.Height, src) - case *blockUnSubscribeMessage: - hbcR.Logger.Debug("receive blockUnSubscribeMessage from peer", "peer", src.ID(), "height", msg.Height) - hbcR.pool.handleUnSubscribeBlock(msg.Height, src) - default: - hbcR.Logger.Error(fmt.Sprintf("Unknown message type %v", reflect.TypeOf(msg))) - } -} - -func (hbcR *BlockchainReactor) SetSwitch(sw *p2p.Switch) { - hbcR.Switch = sw - hbcR.pool.candidatePool.swh = sw -} - -func (hbcR *BlockchainReactor) SwitchToHotSync(state sm.State, blockSynced int32) { - hbcR.pool.SwitchToHotSync(state, blockSynced) -} - -func (hbcR *BlockchainReactor) SwitchToConsensusSync(state sm.State) { - hbcR.pool.SwitchToConsensusSync(&state) -} - -func (hbcR *BlockchainReactor) switchRoutine() { - switchToConsensusTicker := time.NewTicker(switchToConsensusIntervalSeconds * time.Second) - defer switchToConsensusTicker.Stop() - for { - select { - case <-hbcR.Quit(): - return - case <-hbcR.pool.Quit(): - return - case <-switchToConsensusTicker.C: - if hbcR.pool.getSyncPattern() == Hot && hbcR.privValidator != nil && hbcR.pool.state.Validators.HasAddress(hbcR.privValidator.GetAddress()) { - hbcR.Logger.Info("hot sync switching to consensus sync") - conR, ok := hbcR.Switch.Reactor("CONSENSUS").(consensusReactor) - if ok { - hbcR.pool.SwitchToConsensusSync(nil) - conR.SwitchToConsensus(hbcR.pool.state, int(hbcR.pool.getBlockSynced())) - return - } else { - // should only happen during testing - } - } - } - } -} - -func (hbcR *BlockchainReactor) poolRoutine() { - for { - select { - case <-hbcR.Quit(): - return - case <-hbcR.pool.Quit(): - return - case message := <-hbcR.messagesCh: - peer := hbcR.Switch.Peers().Get(message.peerId) - if peer == nil { - continue - } - msgBytes := cdc.MustMarshalBinaryBare(message.blockChainMessage) - hbcR.Logger.Debug(fmt.Sprintf("send message %s", message.blockChainMessage.String()), "peer", peer.ID()) - queued := peer.TrySend(HotBlockchainChannel, msgBytes) - if !queued { - hbcR.Logger.Debug("Send queue is full or no hot sync channel, drop blockChainMessage", "peer", peer.ID()) - } - } - } -} - -// BlockchainMessage is a generic message for this reactor. -type BlockchainMessage interface { - ValidateBasic() error - String() string -} - -//------------------------------------- - -// Subscribe block at specified height range -type blockSubscribeMessage struct { - FromHeight int64 - ToHeight int64 -} - -func (m *blockSubscribeMessage) ValidateBasic() error { - if m.FromHeight < 0 { - return errors.New("Negative Height") - } - if m.ToHeight < m.FromHeight { - return errors.New("Height is greater than ToHeight") - } - if m.ToHeight-m.FromHeight > maxSubscribeBlocks { - return errors.New("Subscribe too many blocks") - } - return nil -} - -func (m *blockSubscribeMessage) String() string { - return fmt.Sprintf("[blockSubscribeMessage from %v, to %v]", m.FromHeight, m.ToHeight) -} - -//------------------------------------- - -// UnSubscribe block at specified height -// Why not in range? -// Because of peer pick strategy, will not subscribe continuous blocks from peer that considered as unknown or bad. -// And because of reschedule strategy, the previous continuous subscription become discontinuous. -type blockUnSubscribeMessage struct { - Height int64 -} - -func (m *blockUnSubscribeMessage) ValidateBasic() error { - if m.Height < 0 { - return errors.New("Negative Height") - } - return nil -} - -func (m *blockUnSubscribeMessage) String() string { - return fmt.Sprintf("[blockUnSubscribeMessage at %v]", m.Height) -} - -//------------------------------------- -type noBlockResponseMessage struct { - Height int64 -} - -func (m *noBlockResponseMessage) ValidateBasic() error { - if m.Height < 0 { - return errors.New("Negative Height") - } - return nil -} - -func (m *noBlockResponseMessage) String() string { - return fmt.Sprintf("[noBlockResponseMessage %v]", m.Height) -} - -//------------------------------------- - -type blockCommitResponseMessage struct { - Block *types.Block - Commit *types.Commit -} - -func (m *blockCommitResponseMessage) Height() int64 { - // have checked, block and commit can't both be nil - if m.Block != nil { - return m.Block.Height - } else { - return m.Commit.Height() - } -} - -func (m *blockCommitResponseMessage) ValidateBasic() error { - if m.Block == nil && m.Commit == nil { - return errors.New("Both Commit and Block field are missing") - } - - if m.Commit != nil && m.Block != nil { - blockId := makeBlockID(m.Block) - if !blockId.Equals(m.Commit.BlockID) { - return errors.New("BlockID mismatch") - } - } - if m.Commit != nil { - if err := m.Commit.ValidateBasic(); err != nil { - return err - } - } - if m.Block != nil { - if err := m.Block.ValidateBasic(); err != nil { - return err - } - } - return nil -} - -func (m *blockCommitResponseMessage) String() string { - return fmt.Sprintf("[blockCommitResponseMessage %v]", m.Height()) -} - -//------------------------------------- - -func RegisterHotBlockchainMessages(cdc *amino.Codec) { - cdc.RegisterInterface((*BlockchainMessage)(nil), nil) - cdc.RegisterConcrete(&blockSubscribeMessage{}, "tendermint/blockchain/hotBlockSubscribe", nil) - cdc.RegisterConcrete(&blockUnSubscribeMessage{}, "tendermint/blockchain/hotBlockUnSubscribe", nil) - cdc.RegisterConcrete(&blockCommitResponseMessage{}, "tendermint/blockchain/hotBlockCommitResponse", nil) - cdc.RegisterConcrete(&noBlockResponseMessage{}, "tendermint/blockchain/hotNoBlockResponse", nil) -} - -func decodeMsg(bz []byte) (msg BlockchainMessage, err error) { - if len(bz) > maxMsgSize { - return msg, fmt.Errorf("Msg exceeds max size (%d > %d)", len(bz), maxMsgSize) - } - err = cdc.UnmarshalBinaryBare(bz, &msg) - return -} diff --git a/blockchain/hot/reactor_test.go b/blockchain/hot/reactor_test.go deleted file mode 100644 index 2d9f58e9f..000000000 --- a/blockchain/hot/reactor_test.go +++ /dev/null @@ -1,260 +0,0 @@ -package hot - -import ( - "os" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/tendermint/tendermint/blockchain" - cfg "github.com/tendermint/tendermint/config" - cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/p2p" - "github.com/tendermint/tendermint/proxy" - sm "github.com/tendermint/tendermint/state" - "github.com/tendermint/tendermint/types" -) - -func TestHotSyncReactorBasic(t *testing.T) { - config = cfg.ResetTestRoot("blockchain_reactor_test") - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - - maxBlockHeight := int64(65) - - reactorPairs := make([]BlockChainReactorPair, 2) - - eventBus1 := types.NewEventBus() - err := eventBus1.Start() - defer eventBus1.Stop() - assert.NoError(t, err) - eventBus2 := types.NewEventBus() - err = eventBus2.Start() - defer eventBus2.Stop() - assert.NoError(t, err) - reactorPairs[0] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, maxBlockHeight, eventBus1, true, false) - reactorPairs[1] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, 0, eventBus2, true, false) - - p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("hot", reactorPairs[i].reactor) - return s - - }, p2p.Connect2Switches) - - defer func() { - for _, r := range reactorPairs { - r.reactor.Stop() - r.app.Stop() - } - }() - - tests := []struct { - height int64 - existent bool - }{ - {maxBlockHeight + 2, false}, - {10, true}, - {1, true}, - {100, false}, - } - - for { - if reactorPairs[1].reactor.pool.currentHeight() == maxBlockHeight-1 { - break - } - time.Sleep(10 * time.Millisecond) - } - - for _, tt := range tests { - block := reactorPairs[1].reactor.pool.store.LoadBlock(tt.height) - if tt.existent { - assert.True(t, block != nil) - } else { - assert.True(t, block == nil) - } - } -} - -func TestHotSyncReactorSwitch(t *testing.T) { - config = cfg.ResetTestRoot("blockchain_reactor_test") - defer os.RemoveAll(config.RootDir) - genDoc, privVals := randGenesisDoc(1, false, 30) - - maxBlockHeight := int64(65) - - reactorPairs := make([]BlockChainReactorPair, 2) - - eventBus0 := types.NewEventBus() - err := eventBus0.Start() - defer eventBus0.Stop() - assert.NoError(t, err) - eventBus1 := types.NewEventBus() - err = eventBus1.Start() - defer eventBus1.Stop() - assert.NoError(t, err) - reactorPairs[0] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, maxBlockHeight, eventBus0, true, false) - reactorPairs[1] = newBlockchainReactorPair(log.TestingLogger(), genDoc, privVals, 0, eventBus1, true, true) - - p2p.MakeConnectedSwitches(config.P2P, 2, func(i int, s *p2p.Switch) *p2p.Switch { - s.AddReactor("hot", reactorPairs[i].reactor) - return s - - }, p2p.Connect2Switches) - - defer func() { - for _, r := range reactorPairs { - r.reactor.Stop() - r.app.Stop() - } - }() - - time.Sleep(100 * time.Millisecond) - assert.Equal(t, reactorPairs[1].reactor.pool.currentHeight(), int64(0)) - - for i := int64(0); i < 10; i++ { - first := reactorPairs[0].reactor.pool.store.LoadBlock(i + 1) - bId := makeBlockID(first) - second := reactorPairs[0].reactor.pool.store.LoadBlock(i + 2) - reactorPairs[1].reactor.pool.applyBlock(&blockState{block: first, commit: second.LastCommit, blockId: bId}) - } - - reactorPairs[1].reactor.SwitchToHotSync(reactorPairs[1].reactor.pool.state, 10) - - tests := []struct { - height int64 - existent bool - }{ - {maxBlockHeight + 2, false}, - {10, true}, - {1, true}, - {100, false}, - } - - for { - if reactorPairs[1].reactor.pool.currentHeight() == maxBlockHeight-1 { - break - } - time.Sleep(10 * time.Millisecond) - } - - for _, tt := range tests { - block := reactorPairs[1].reactor.pool.store.LoadBlock(tt.height) - if tt.existent { - assert.True(t, block != nil) - } else { - assert.True(t, block == nil) - } - } - - reactorPairs[0].reactor.SwitchToConsensusSync(reactorPairs[0].reactor.pool.state) - - pool := reactorPairs[0].reactor.pool - consensusHeight := int64(100) - for i := int64(0); i < consensusHeight; i++ { - height := pool.currentHeight() - block, commit, vs := nextBlock(pool.state, pool.store, pool.blockExec, reactorPairs[0].privVals[0]) - - err := eventBus0.PublishEventCompleteProposal(types.EventDataCompleteProposal{Height: block.Height, Block: *block}) - assert.NoError(t, err) - err = eventBus0.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{Height: block.Height, Votes: *vs}) - assert.NoError(t, err) - pool.applyBlock(&blockState{ - block: block, - commit: commit, - blockId: makeBlockID(block), - }) - for { - if height+1 != pool.currentHeight() { - time.Sleep(10 * time.Millisecond) - } else { - break - } - } - } - - tests2 := []struct { - height int64 - existent bool - }{ - {reactorPairs[1].reactor.pool.currentHeight(), true}, - {122, true}, - {50, true}, - {200, false}, - } - - for { - if reactorPairs[1].reactor.pool.currentHeight() == maxBlockHeight+consensusHeight { - break - } - time.Sleep(10 * time.Millisecond) - } - - for _, tt := range tests2 { - block := reactorPairs[1].reactor.pool.store.LoadBlock(tt.height) - if tt.existent { - assert.True(t, block != nil) - } else { - assert.True(t, block == nil) - } - } -} - -type BlockChainReactorPair struct { - reactor *BlockchainReactor - app proxy.AppConns - privVals []types.PrivValidator -} - -func newBlockchainReactorPair(logger log.Logger, genDoc *types.GenesisDoc, privVals []types.PrivValidator, maxBlockHeight int64, eventBus *types.EventBus, hotsync, fastSync bool) BlockChainReactorPair { - if len(privVals) != 1 { - panic("only support one validator") - } - app := &testApp{} - cc := proxy.NewLocalClientCreator(app) - proxyApp := proxy.NewAppConns(cc) - err := proxyApp.Start() - if err != nil { - panic(cmn.ErrorWrap(err, "error start app")) - } - blockDB := dbm.NewMemDB() - stateDB := dbm.NewMemDB() - blockStore := blockchain.NewBlockStore(blockDB) - state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) - if err != nil { - panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) - } - - // Make the BlockPool itself. - // NOTE we have to create and commit the blocks first because - // pool.height is determined from the store. - blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), proxyApp.Consensus(), - sm.MockMempool{}, sm.MockEvidencePool{}, true) - // let's add some blocks in - for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { - lastCommit := types.NewCommit(types.BlockID{}, nil) - if blockHeight > 1 { - lastBlockMeta := blockStore.LoadBlockMeta(blockHeight - 1) - lastBlock := blockStore.LoadBlock(blockHeight - 1) - - vote := makeVote(&lastBlock.Header, lastBlockMeta.BlockID, state.Validators, privVals[0]).CommitSig() - lastCommit = types.NewCommit(lastBlockMeta.BlockID, []*types.CommitSig{vote}) - } - - thisBlock := makeBlock(blockHeight, state, lastCommit) - - thisParts := thisBlock.MakePartSet(types.BlockPartSizeBytes) - blockID := types.BlockID{thisBlock.Hash(), thisParts.Header()} - - state, err = blockExec.ApplyBlock(state, blockID, thisBlock) - if err != nil { - panic(cmn.ErrorWrap(err, "error apply block")) - } - blockStore.SaveBlock(thisBlock, thisParts, lastCommit) - } - bcReactor := NewBlockChainReactor(state.Copy(), blockExec, blockStore, hotsync, fastSync, 2*time.Second, WithEventBus(eventBus)) - bcReactor.SetLogger(logger.With("module", "hotsync")) - return BlockChainReactorPair{bcReactor, proxyApp, privVals} -} diff --git a/blockchain/hot/wire.go b/blockchain/hot/wire.go deleted file mode 100644 index f90343024..000000000 --- a/blockchain/hot/wire.go +++ /dev/null @@ -1,13 +0,0 @@ -package hot - -import ( - "github.com/tendermint/go-amino" - "github.com/tendermint/tendermint/types" -) - -var cdc = amino.NewCodec() - -func init() { - RegisterHotBlockchainMessages(cdc) - types.RegisterBlockAmino(cdc) -} diff --git a/blockchain/pool.go b/blockchain/pool.go index 29516a1b6..f641ec623 100644 --- a/blockchain/pool.go +++ b/blockchain/pool.go @@ -450,7 +450,7 @@ func (peer *bpPeer) setLogger(l log.Logger) { } func (peer *bpPeer) resetMonitor() { - peer.recvMonitor = flow.New(time.Second, time.Second*types.MonitorWindowInSeconds) + peer.recvMonitor = flow.New(time.Second, time.Second * types.MonitorWindowInSeconds) initialValue := float64(minRecvRate) * math.E peer.recvMonitor.SetREMA(initialValue) } diff --git a/blockchain/reactor.go b/blockchain/reactor.go index 4b44f745c..1f39c1afb 100644 --- a/blockchain/reactor.go +++ b/blockchain/reactor.go @@ -6,7 +6,8 @@ import ( "reflect" "time" - "github.com/tendermint/go-amino" + amino "github.com/tendermint/go-amino" + "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" sm "github.com/tendermint/tendermint/state" @@ -14,7 +15,7 @@ import ( ) const ( - // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) under fast sync mode. + // BlockchainChannel is a channel for blocks and status updates (`BlockStore` height) BlockchainChannel = byte(0x40) trySyncIntervalMS = 10 @@ -42,11 +43,6 @@ type consensusReactor interface { SwitchToConsensus(sm.State, int) } -type hotsyncReactor interface { - SwitchToHotSync(sm.State, int32) - SwitchToConsensusSync(sm.State) -} - type peerError struct { err error peerID p2p.ID @@ -63,12 +59,10 @@ type BlockchainReactor struct { // immutable initialState sm.State - blockExec *sm.BlockExecutor - store *BlockStore - pool *BlockPool - fastSync bool - hotSyncReactor bool - hotsync bool + blockExec *sm.BlockExecutor + store *BlockStore + pool *BlockPool + fastSync bool requestsCh <-chan BlockRequest errorsCh <-chan peerError @@ -76,7 +70,7 @@ type BlockchainReactor struct { // NewBlockchainReactor returns new reactor instance. func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *BlockStore, - fastSync, hotSyncReactor, hotSync bool) *BlockchainReactor { + fastSync bool) *BlockchainReactor { if state.LastBlockHeight != store.Height() { panic(fmt.Sprintf("state (%v) and store (%v) height mismatch", state.LastBlockHeight, @@ -95,15 +89,13 @@ func NewBlockchainReactor(state sm.State, blockExec *sm.BlockExecutor, store *Bl ) bcR := &BlockchainReactor{ - initialState: state, - blockExec: blockExec, - store: store, - pool: pool, - fastSync: fastSync, - requestsCh: requestsCh, - errorsCh: errorsCh, - hotSyncReactor: hotSyncReactor, - hotsync: hotSync, + initialState: state, + blockExec: blockExec, + store: store, + pool: pool, + fastSync: fastSync, + requestsCh: requestsCh, + errorsCh: errorsCh, } bcR.BaseReactor = *p2p.NewBaseReactor("BlockchainReactor", bcR) return bcR @@ -301,33 +293,13 @@ FOR_LOOP: // we need make sure blockstore has a block because when switch to consensus, it will verify the commit between block and state // refer to `cs.blockStore.LoadSeenCommit(state.LastBlockHeight)` if bcR.pool.IsCaughtUp() && (height == bcR.pool.initHeight || blocksSynced > 0) { + bcR.Logger.Info("Time to switch to consensus reactor!", "height", height) bcR.pool.Stop() - if bcR.hotSyncReactor && bcR.hotsync { - bcR.Logger.Info("Time to switch to hot sync reactor!", "height", height) - hotR, ok := bcR.Switch.Reactor("HOT").(hotsyncReactor) - if ok { - hotR.SwitchToHotSync(state, int32(blocksSynced)) - } else { - // should only happen during testing - } - + conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) + if ok { + conR.SwitchToConsensus(state, blocksSynced) } else { - if bcR.hotSyncReactor { - bcR.Logger.Info("Time to switch hot sync reactor to consensus sync pattern!", "height", height) - hotR, ok := bcR.Switch.Reactor("HOT").(hotsyncReactor) - if ok { - hotR.SwitchToConsensusSync(state) - } else { - // should only happen during testing - } - } - bcR.Logger.Info("Time to switch to consensus reactor!", "height", height) - conR, ok := bcR.Switch.Reactor("CONSENSUS").(consensusReactor) - if ok { - conR.SwitchToConsensus(state, blocksSynced) - } else { - // should only happen during testing - } + // should only happen during testing } break FOR_LOOP diff --git a/blockchain/reactor_test.go b/blockchain/reactor_test.go index 6c46a4a38..a7be6367d 100644 --- a/blockchain/reactor_test.go +++ b/blockchain/reactor_test.go @@ -118,7 +118,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals blockStore.SaveBlock(thisBlock, thisParts, lastCommit) } - bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync, false, false) + bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) bcReactor.SetLogger(logger.With("module", "blockchain")) return BlockchainReactorPair{bcReactor, proxyApp} @@ -255,6 +255,21 @@ func TestBadBlockStopsPeer(t *testing.T) { assert.True(t, lastReactorPair.reactor.Switch.Peers().Size() < len(reactorPairs)-1) } +//---------------------------------------------- +// utility funcs + +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < 10; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) + return block +} + type testApp struct { abci.BaseApplication } diff --git a/blockchain/store_test.go b/blockchain/store_test.go index 28ab6051a..bd30bc6d2 100644 --- a/blockchain/store_test.go +++ b/blockchain/store_test.go @@ -45,21 +45,6 @@ func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFu return state, NewBlockStore(blockDB), func() { os.RemoveAll(config.RootDir) } } -//---------------------------------------------- -// utility funcs - -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < 10; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) - return block -} - func TestLoadBlockStoreStateJSON(t *testing.T) { db := db.NewMemDB() diff --git a/config/config.go b/config/config.go index a3b91b302..b83b0492d 100644 --- a/config/config.go +++ b/config/config.go @@ -164,18 +164,6 @@ type BaseConfig struct { // and verifying their commits FastSync bool `mapstructure:"fast_sync"` - // it is for fullnode/witness who do not need consensus to sync block. - HotSyncReactor bool `mapstructure:"hot_sync_reactor"` - - // only take effect when HotSyncReactor is true. - // If true, will sync blocks use hot sync protocol - // If false, still use tendermint consensus protocol, but can still handle other peers sync request. - HotSync bool `mapstructure:"hot_sync"` - - // the max wait time for subscribe a block. - // only take effect when hot_sync is true - HotSyncTimeout time.Duration `mapstructure:"hot_sync_timeout"` - // As state sync is an experimental feature, this switch can totally disable it on core network nodes (validator, witness) StateSyncReactor bool `mapstructure:"state_sync_reactor"` @@ -244,9 +232,6 @@ func DefaultBaseConfig() BaseConfig { ProfListenAddress: "", FastSync: true, StateSyncReactor: true, - HotSync: false, - HotSyncReactor: false, - HotSyncTimeout: 3 * time.Second, StateSyncHeight: -1, FilterPeers: false, DBBackend: "leveldb", @@ -308,9 +293,6 @@ func (cfg BaseConfig) ValidateBasic() error { default: return errors.New("unknown log_format (must be 'plain' or 'json')") } - if !cfg.HotSyncReactor && cfg.HotSync { - return errors.New("config hot_sync can't be true while hot_sync_reactor is false") - } return nil } @@ -744,15 +726,14 @@ func (cfg *DBCacheConfig) ToGolevelDBOpt() *optPkg.Options { // MempoolConfig defines the configuration options for the Tendermint mempool type MempoolConfig struct { - RootDir string `mapstructure:"home"` - Recheck bool `mapstructure:"recheck"` - Broadcast bool `mapstructure:"broadcast"` - WalPath string `mapstructure:"wal_dir"` - Size int `mapstructure:"size"` - MaxTxsBytes int64 `mapstructure:"max_txs_bytes"` - CacheSize int `mapstructure:"cache_size"` - OnlyToPersistent bool `mapstructure:"only_to_persistent"` - SkipTxFromPersistent bool `mapstructure:"skip_tx_from_persistent"` + RootDir string `mapstructure:"home"` + Recheck bool `mapstructure:"recheck"` + Broadcast bool `mapstructure:"broadcast"` + WalPath string `mapstructure:"wal_dir"` + Size int `mapstructure:"size"` + MaxTxsBytes int64 `mapstructure:"max_txs_bytes"` + CacheSize int `mapstructure:"cache_size"` + OnlyPersistent bool `mapstructure:"only_persistent"` } // DefaultMempoolConfig returns a default configuration for the Tendermint mempool @@ -763,11 +744,10 @@ func DefaultMempoolConfig() *MempoolConfig { WalPath: "", // Each signature verification takes .5ms, Size reduced until we implement // ABCI Recheck - Size: 5000, - MaxTxsBytes: 1024 * 1024 * 1024, // 1GB - CacheSize: 10000, - OnlyToPersistent: false, - SkipTxFromPersistent: false, + Size: 5000, + MaxTxsBytes: 1024 * 1024 * 1024, // 1GB + CacheSize: 10000, + OnlyPersistent: false, } } diff --git a/config/toml.go b/config/toml.go index c60907efa..a2b55c36d 100644 --- a/config/toml.go +++ b/config/toml.go @@ -81,18 +81,6 @@ moniker = "{{ .BaseConfig.Moniker }}" # and verifying their commits fast_sync = {{ .BaseConfig.FastSync }} -# Only take effect when HotSyncReactor is true. -# If true, will sync blocks use hot sync protocol -# If false, still use tendermint consensus protocol, but can still handle other peers sync request. -hot_sync = {{.BaseConfig.HotSync}} - -# The max wait time for subscribe a block. -# Only take effect when hot_sync is true -hot_sync_timeout = "{{.BaseConfig.HotSyncTimeout}}" - -# It will benefit fullnode and witness who do not need consensus by saving network and cpu resources. -# Recommend the node that is not validator to turn on. -hot_sync_reactor = {{.BaseConfig.HotSyncReactor}} # As state sync is an experimental feature, this switch can totally disable it on core network nodes (validator, witness) state_sync_reactor = {{ .BaseConfig.StateSyncReactor }} @@ -334,10 +322,7 @@ broadcast = {{ .Mempool.Broadcast }} wal_dir = "{{ js .Mempool.WalPath }}" # If set true, will only broadcast transactions to persistent peers. -only_to_persistent = {{ .Mempool.OnlyToPersistent }} - -# If set true, only the transaction from none persistent peer will broadcast. -skip_tx_from_persistent = {{ .Mempool.SkipTxFromPersistent }} +only_persistent = {{ .Mempool.OnlyPersistent }} # Maximum number of transactions in the mempool size = {{ .Mempool.Size }} diff --git a/consensus/reactor.go b/consensus/reactor.go index 61001e938..035c8f413 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -971,13 +971,6 @@ func (ps *PeerState) GetHeight() int64 { return ps.PRS.Height } -// use by hotsync to ensure mempool can broadcast tx to it's peer -func (ps *PeerState) SetHeight(height int64) { - ps.mtx.Lock() - defer ps.mtx.Unlock() - ps.PRS.Height = height -} - // SetHasProposal sets the given proposal as known for the peer. func (ps *PeerState) SetHasProposal(proposal *types.Proposal) { ps.mtx.Lock() diff --git a/consensus/state.go b/consensus/state.go index 0422890b6..3cab6551b 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1661,12 +1661,6 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, blockID, ok := precommits.TwoThirdsMajority() if ok { - cs.eventBus.PublishEventMajorPrecommits(types.EventDataMajorPrecommits{ - Height: cs.Height, - Round: cs.Round, - Step: cs.Step.String(), - Votes: *precommits.Copy(), - }) // Executed as TwoThirdsMajority could be from a higher round cs.enterNewRound(height, vote.Round) cs.enterPrecommit(height, vote.Round) diff --git a/consensus/types/round_state.go b/consensus/types/round_state.go index 63e485db2..c4372e201 100644 --- a/consensus/types/round_state.go +++ b/consensus/types/round_state.go @@ -143,7 +143,6 @@ func (rs *RoundState) CompleteProposalEvent() types.EventDataCompleteProposal { Round: rs.Round, Step: rs.Step.String(), BlockID: blockId, - Block: *rs.ProposalBlock, } } diff --git a/mempool/mempool.go b/mempool/mempool.go index df7bad082..03f086c6f 100644 --- a/mempool/mempool.go +++ b/mempool/mempool.go @@ -37,8 +37,6 @@ type TxInfo struct { // We don't use p2p.ID here because it's too big. The gain is to store max 2 // bytes with each tx to identify the sender rather than 20 bytes. PeerID uint16 - // whether the tx comes from a persistent peer. - FromPersistent bool } /* @@ -370,7 +368,7 @@ func (mem *Mempool) TxsWaitChan() <-chan struct{} { // It gets called from another goroutine. // CONTRACT: Either cb will get called, or err returned. func (mem *Mempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err error) { - return mem.CheckTxWithInfo(tx, cb, TxInfo{PeerID: UnknownPeerID, FromPersistent: false}) + return mem.CheckTxWithInfo(tx, cb, TxInfo{PeerID: UnknownPeerID}) } // CheckTxWithInfo performs the same operation as CheckTx, but with extra meta data about the tx. @@ -443,7 +441,7 @@ func (mem *Mempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), txInfo } reqRes := mem.proxyAppConn.CheckTxAsync(tx) - reqRes.SetCallback(mem.reqResCb(tx, txInfo, cb)) + reqRes.SetCallback(mem.reqResCb(tx, txInfo.PeerID, cb)) return nil } @@ -476,14 +474,14 @@ func (mem *Mempool) globalCb(req *abci.Request, res *abci.Response) { // when all other response processing is complete. // // Used in CheckTxWithInfo to record PeerID who sent us the tx. -func (mem *Mempool) reqResCb(tx []byte, txInfo TxInfo, externalCb func(*abci.Response)) func(res *abci.Response) { +func (mem *Mempool) reqResCb(tx []byte, peerID uint16, externalCb func(*abci.Response)) func(res *abci.Response) { return func(res *abci.Response) { if mem.recheckCursor != nil { // this should never happen panic("recheck cursor is not nil in reqResCb") } - mem.resCbFirstTime(tx, txInfo, res) + mem.resCbFirstTime(tx, peerID, res) // update metrics mem.metrics.Size.Set(float64(mem.Size())) @@ -522,7 +520,7 @@ func (mem *Mempool) removeTx(tx types.Tx, elem *clist.CElement, removeFromCache // // The case where the app checks the tx for the second and subsequent times is // handled by the resCbRecheck callback. -func (mem *Mempool) resCbFirstTime(tx []byte, txInfo TxInfo, res *abci.Response) { +func (mem *Mempool) resCbFirstTime(tx []byte, peerID uint16, res *abci.Response) { switch r := res.Value.(type) { case *abci.Response_CheckTx: var postCheckErr error @@ -531,19 +529,17 @@ func (mem *Mempool) resCbFirstTime(tx []byte, txInfo TxInfo, res *abci.Response) } if (r.CheckTx.Code == abci.CodeTypeOK) && postCheckErr == nil { memTx := &mempoolTx{ - fromPersistent: txInfo.FromPersistent, - height: mem.height, - gasWanted: r.CheckTx.GasWanted, - tx: tx, + height: mem.height, + gasWanted: r.CheckTx.GasWanted, + tx: tx, } - memTx.senders.Store(txInfo.PeerID, true) + memTx.senders.Store(peerID, true) mem.addTx(memTx) mem.logger.Info("Added good transaction", "tx", TxID(tx), "res", r, "height", memTx.height, "total", mem.Size(), - "fromPersistent", memTx.fromPersistent, ) mem.notifyTxsAvailable() } else { @@ -782,10 +778,9 @@ func (mem *Mempool) recheckTxs(txs []types.Tx) { // mempoolTx is a transaction that successfully ran type mempoolTx struct { - fromPersistent bool // whether the tx come from a persistent peer - height int64 // height that this tx had been validated in - gasWanted int64 // amount of gas this tx states it will require - tx types.Tx // + height int64 // height that this tx had been validated in + gasWanted int64 // amount of gas this tx states it will require + tx types.Tx // // ids of peers who've sent us this tx (as a map for quick lookups). // senders: PeerID -> bool diff --git a/mempool/reactor.go b/mempool/reactor.go index 8293f3852..6f716ecf8 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -7,7 +7,8 @@ import ( "sync" "time" - "github.com/tendermint/go-amino" + amino "github.com/tendermint/go-amino" + cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/libs/clist" "github.com/tendermint/tendermint/libs/log" @@ -192,8 +193,9 @@ func (memR *MempoolReactor) receiveImpl(chID byte, src p2p.Peer, msgBytes []byte switch msg := msg.(type) { case *TxMessage: + memR.Mempool.metrics.ReceivedTx.With("peer_id", string(src.ID())).Add(1) peerID := memR.ids.GetForPeer(src) - err := memR.Mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{PeerID: peerID, FromPersistent: memR.Switch.IsPersistent(src)}) + err := memR.Mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{PeerID: peerID}) if err != nil { if err == ErrTxInCache { memR.Mempool.metrics.DuplicateTx.With("peer_id", string(src.ID())).Add(1) @@ -213,7 +215,7 @@ type PeerState interface { // Send new mempool txs to peer. func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { - if !memR.config.Broadcast || (memR.config.OnlyToPersistent && !memR.Switch.IsPersistent(peer)) { + if !memR.config.Broadcast || (memR.config.OnlyPersistent && !memR.Switch.IsPersistent(peer)) { return } @@ -239,34 +241,34 @@ func (memR *MempoolReactor) broadcastTxRoutine(peer p2p.Peer) { return } } + memTx := next.Value.(*mempoolTx) - if !memR.config.SkipTxFromPersistent || !memTx.fromPersistent { - // make sure the peer is up to date - peerState, ok := peer.Get(types.PeerStateKey).(PeerState) - if !ok { - // Peer does not have a state yet. We set it in the consensus reactor, but - // when we add peer in Switch, the order we call reactors#AddPeer is - // different every time due to us using a map. Sometimes other reactors - // will be initialized before the consensus reactor. We should wait a few - // milliseconds and retry. - time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) - continue - } - if peerState.GetHeight() < memTx.Height()-1 { // Allow for a lag of 1 block + + // make sure the peer is up to date + peerState, ok := peer.Get(types.PeerStateKey).(PeerState) + if !ok { + // Peer does not have a state yet. We set it in the consensus reactor, but + // when we add peer in Switch, the order we call reactors#AddPeer is + // different every time due to us using a map. Sometimes other reactors + // will be initialized before the consensus reactor. We should wait a few + // milliseconds and retry. + time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) + continue + } + if peerState.GetHeight() < memTx.Height()-1 { // Allow for a lag of 1 block + time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) + continue + } + + // ensure peer hasn't already sent us this tx + if _, ok := memTx.senders.Load(peerID); !ok { + // send memTx + msg := &TxMessage{Tx: memTx.tx} + success := peer.Send(MempoolChannel, cdc.MustMarshalBinaryBare(msg)) + if !success { time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) continue } - - // ensure peer hasn't already sent us this tx - if _, ok := memTx.senders.Load(peerID); !ok { - // send memTx - msg := &TxMessage{Tx: memTx.tx} - success := peer.Send(MempoolChannel, cdc.MustMarshalBinaryBare(msg)) - if !success { - time.Sleep(peerCatchupSleepIntervalMS * time.Millisecond) - continue - } - } } select { diff --git a/node/node.go b/node/node.go index cd0bc30b8..60da4071a 100644 --- a/node/node.go +++ b/node/node.go @@ -16,10 +16,9 @@ import ( "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/rs/cors" - "github.com/tendermint/go-amino" + amino "github.com/tendermint/go-amino" abci "github.com/tendermint/tendermint/abci/types" bc "github.com/tendermint/tendermint/blockchain" - "github.com/tendermint/tendermint/blockchain/hot" cfg "github.com/tendermint/tendermint/config" cs "github.com/tendermint/tendermint/consensus" "github.com/tendermint/tendermint/crypto/ed25519" @@ -38,8 +37,8 @@ import ( ctypes "github.com/tendermint/tendermint/rpc/core/types" grpccore "github.com/tendermint/tendermint/rpc/grpc" rpcserver "github.com/tendermint/tendermint/rpc/lib/server" - "github.com/tendermint/tendermint/snapshot" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/snapshot" "github.com/tendermint/tendermint/state/blockindex" bkv "github.com/tendermint/tendermint/state/blockindex/kv" nullblk "github.com/tendermint/tendermint/state/blockindex/null" @@ -124,20 +123,19 @@ func DefaultNewNode(config *cfg.Config, logger log.Logger) (*Node, error) { } // MetricsProvider returns a consensus, p2p and mempool Metrics. -type MetricsProvider func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics, *hot.Metrics) +type MetricsProvider func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics) // DefaultMetricsProvider returns Metrics build using Prometheus client library // if Prometheus is enabled. Otherwise, it returns no-op Metrics. func DefaultMetricsProvider(config *cfg.InstrumentationConfig) MetricsProvider { - return func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics, *hot.Metrics) { + return func(chainID string) (*cs.Metrics, *p2p.Metrics, *mempl.Metrics, *sm.Metrics) { if config.Prometheus { return cs.PrometheusMetrics(config.Namespace, "chain_id", chainID), p2p.PrometheusMetrics(config.Namespace, "chain_id", chainID), mempl.PrometheusMetrics(config.Namespace, "chain_id", chainID), - sm.PrometheusMetrics(config.Namespace, "chain_id", chainID), - hot.PrometheusMetrics(config.Namespace, "chain_id", chainID) + sm.PrometheusMetrics(config.Namespace, "chain_id", chainID) } - return cs.NopMetrics(), p2p.NopMetrics(), mempl.NopMetrics(), sm.NopMetrics(), hot.NopMetrics() + return cs.NopMetrics(), p2p.NopMetrics(), mempl.NopMetrics(), sm.NopMetrics() } } @@ -162,22 +160,21 @@ type Node struct { isListening bool // services - eventBus *types.EventBus // pub/sub for services - stateDB dbm.DB - blockStore *bc.BlockStore // store the blockchain to disk - bcReactor *bc.BlockchainReactor // for fast-syncing - mempoolReactor *mempl.MempoolReactor // for gossipping transactions - consensusState *cs.ConsensusState // latest consensus state - consensusReactor *cs.ConsensusReactor // for participating in the consensus - evidencePool *evidence.EvidencePool // tracking evidence - proxyApp proxy.AppConns // connection to the application - rpcListeners []net.Listener // rpc servers - txIndexer txindex.TxIndexer - blockIndexer blockindex.BlockIndexer - indexerService *txindex.IndexerService + eventBus *types.EventBus // pub/sub for services + stateDB dbm.DB + blockStore *bc.BlockStore // store the blockchain to disk + bcReactor *bc.BlockchainReactor // for fast-syncing + mempoolReactor *mempl.MempoolReactor // for gossipping transactions + consensusState *cs.ConsensusState // latest consensus state + consensusReactor *cs.ConsensusReactor // for participating in the consensus + evidencePool *evidence.EvidencePool // tracking evidence + proxyApp proxy.AppConns // connection to the application + rpcListeners []net.Listener // rpc servers + txIndexer txindex.TxIndexer + blockIndexer blockindex.BlockIndexer + indexerService *txindex.IndexerService blockIndexService *blockindex.IndexerService - indexHub *sm.IndexHub - prometheusSrv *http.Server + prometheusSrv *http.Server } // NewNode returns a new, ready to go, Tendermint Node. @@ -242,7 +239,7 @@ func NewNode(config *cfg.Config, // Transaction indexing var txIndexer txindex.TxIndexer - var txDB dbm.DB // TODO: remove by refactor defaultdbprovider to cache the created db instaces + var txDB dbm.DB // TODO: remove by refactor defaultdbprovider to cache the created db instaces switch config.TxIndex.Indexer { case "kv": store, err := dbProvider(&DBContext{"tx_index", config}) @@ -289,17 +286,6 @@ func NewNode(config *cfg.Config, return nil, err } - csMetrics, p2pMetrics, memplMetrics, smMetrics, hotMetrics := metricsProvider(genDoc.ChainID) - - indexHub := sm.NewIndexHub(state.LastBlockHeight, stateDB, blockStore, eventBus, sm.IndexHubWithMetrics(smMetrics)) - indexHub.RegisterIndexSvc(blockIndexerService) - indexHub.RegisterIndexSvc(txIndexerService) - indexHub.SetLogger(logger.With("module", "indexer_hub")) - err = indexHub.Start() - if err != nil { - return nil, err - } - // Create the handshaker, which calls RequestInfo, sets the AppVersion on the state, // and replays any blocks as necessary to sync tendermint with the app. consensusLogger := logger.With("module", "consensus") @@ -360,6 +346,8 @@ func NewNode(config *cfg.Config, consensusLogger.Info("This node is not a validator", "addr", addr, "pubKey", pubKey) } + csMetrics, p2pMetrics, memplMetrics, smMetrics := metricsProvider(genDoc.ChainID) + // Make MempoolReactor mempool := mempl.NewMempool( config.Mempool, @@ -425,19 +413,9 @@ func NewNode(config *cfg.Config, ) // Make BlockchainReactor - bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync && (config.StateSyncHeight < 0), config.HotSyncReactor, config.HotSync) + bcReactor := bc.NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync && (config.StateSyncHeight < 0)) bcReactor.SetLogger(logger.With("module", "blockchain")) - var hotSyncReactor *hot.BlockchainReactor - if config.HotSyncReactor { - hotSyncLogger := logger.With("module", "hotsync") - hotSyncReactor = hot.NewBlockChainReactor(state.Copy(), blockExec, blockStore, config.HotSync, fastSync || config.StateSyncHeight >= 0, config.HotSyncTimeout, hot.WithMetrics(hotMetrics), hot.WithEventBus(eventBus)) - hotSyncReactor.SetLogger(hotSyncLogger) - if privValidator != nil { - hotSyncReactor.SetPrivValidator(privValidator) - } - } - // Make ConsensusReactor consensusState := cs.NewConsensusState( config.Consensus, @@ -452,7 +430,7 @@ func NewNode(config *cfg.Config, if privValidator != nil { consensusState.SetPrivValidator(privValidator) } - consensusReactor := cs.NewConsensusReactor(consensusState, fastSync || (config.StateSyncHeight >= 0) || config.HotSync, cs.ReactorMetrics(csMetrics)) + consensusReactor := cs.NewConsensusReactor(consensusState, fastSync || (config.StateSyncHeight >= 0), cs.ReactorMetrics(csMetrics)) consensusReactor.SetLogger(consensusLogger) // services which will be publishing and/or subscribing for messages (events) @@ -541,9 +519,6 @@ func NewNode(config *cfg.Config, if config.StateSyncReactor { sw.AddReactor("STATE", stateReactor) } - if config.HotSyncReactor { - sw.AddReactor("HOT", hotSyncReactor) - } sw.AddReactor("BLOCKCHAIN", bcReactor) sw.AddReactor("CONSENSUS", consensusReactor) sw.AddReactor("EVIDENCE", evidenceReactor) @@ -608,20 +583,19 @@ func NewNode(config *cfg.Config, nodeInfo: nodeInfo, nodeKey: nodeKey, - stateDB: stateDB, - blockStore: blockStore, - bcReactor: bcReactor, - mempoolReactor: mempoolReactor, - consensusState: consensusState, - consensusReactor: consensusReactor, - evidencePool: evidencePool, - proxyApp: proxyApp, - txIndexer: txIndexer, - blockIndexer: blockIndexer, - indexerService: txIndexerService, + stateDB: stateDB, + blockStore: blockStore, + bcReactor: bcReactor, + mempoolReactor: mempoolReactor, + consensusState: consensusState, + consensusReactor: consensusReactor, + evidencePool: evidencePool, + proxyApp: proxyApp, + txIndexer: txIndexer, + blockIndexer: blockIndexer, + indexerService: txIndexerService, blockIndexService: blockIndexerService, - indexHub: indexHub, - eventBus: eventBus, + eventBus: eventBus, } node.BaseService = *cmn.NewBaseService(logger, "Node", node) return node, nil @@ -692,7 +666,6 @@ func (n *Node) OnStop() { n.eventBus.Stop() n.indexerService.Stop() n.blockIndexService.Stop() - n.indexHub.Stop() // now stop the reactors // TODO: gracefully disconnect from peers. @@ -746,7 +719,6 @@ func (n *Node) ConfigureRPC() { rpccore.SetProxyAppQuery(n.proxyApp.Query()) rpccore.SetTxIndexer(n.txIndexer) rpccore.SetBlockIndexer(n.blockIndexer) - rpccore.SetIndexHub(n.indexHub) rpccore.SetConsensusReactor(n.consensusReactor) rpccore.SetEventBus(n.eventBus) rpccore.SetLogger(n.Logger.With("module", "rpc")) @@ -766,7 +738,7 @@ func (n *Node) startRPC() ([]net.Listener, error) { // we may expose the rpc over both a unix and tcp socket listeners := make([]net.Listener, len(listenAddrs)) var wsWorkerPool *gopool.Pool - if n.config.RPC.WebsocketPoolMaxSize > 1 { + if n.config.RPC.WebsocketPoolMaxSize > 1{ wsWorkerPool = gopool.NewPool(n.config.RPC.WebsocketPoolMaxSize, n.config.RPC.WebsocketPoolQueueSize, n.config.RPC.WebsocketPoolSpawnSize) wsWorkerPool.SetLogger(n.Logger.With("module", "routine-pool")) } @@ -977,10 +949,6 @@ func makeNodeInfo( nodeInfo.Channels = append(nodeInfo.Channels, pex.PexChannel) } - if config.HotSyncReactor { - nodeInfo.Channels = append(nodeInfo.Channels, hot.HotBlockchainChannel) - } - lAddr := config.P2P.ExternalAddress if lAddr == "" { diff --git a/p2p/conn/connection.go b/p2p/conn/connection.go index 207e3f7fd..289769f5b 100644 --- a/p2p/conn/connection.go +++ b/p2p/conn/connection.go @@ -322,6 +322,8 @@ func (c *MConnection) Send(chID byte, msgBytes []byte) bool { return false } + c.Logger.Debug("Send", "channel", chID, "conn", c, "msgBytes", fmt.Sprintf("%X", msgBytes)) + // Send message to channel. channel, ok := c.channelsIdx[chID] if !ok { @@ -592,6 +594,7 @@ FOR_LOOP: break FOR_LOOP } if msgBytes != nil { + c.Logger.Debug("Received bytes", "chID", pkt.ChannelID, "msgBytes", fmt.Sprintf("%X", msgBytes)) // NOTE: This means the reactor.Receive runs in the same thread as the p2p recv routine // except the mempool actually is using an asynchronus Receive() to prevent jamming requests // stopping block producing (tested via ) diff --git a/p2p/peer.go b/p2p/peer.go index ac5072933..fab3b42d4 100644 --- a/p2p/peer.go +++ b/p2p/peer.go @@ -247,6 +247,9 @@ func (p *peer) Send(chID byte, msgBytes []byte) bool { return false } res := p.mconn.Send(chID, msgBytes) + if res { + p.metrics.PeerSendBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) + } return res } @@ -259,6 +262,9 @@ func (p *peer) TrySend(chID byte, msgBytes []byte) bool { return false } res := p.mconn.TrySend(chID, msgBytes) + if res { + p.metrics.PeerSendBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) + } return res } @@ -363,6 +369,7 @@ func createMConnection( // which does onPeerError. panic(fmt.Sprintf("Unknown channel %X", chID)) } + p.metrics.PeerReceiveBytesTotal.With("peer_id", string(p.ID())).Add(float64(len(msgBytes))) reactor.Receive(chID, p, msgBytes) } diff --git a/p2p/switch.go b/p2p/switch.go index a849fc90b..0a5d3cf90 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -69,17 +69,16 @@ type PeerFilterFunc func(IPeerSet, Peer) error type Switch struct { cmn.BaseService - config *config.P2PConfig - reactors map[string]Reactor - chDescs []*conn.ChannelDescriptor - reactorsByCh map[byte]Reactor - peers *PeerSet - dialing *cmn.CMap - reconnecting *cmn.CMap - nodeInfo NodeInfo // our node info - nodeKey *NodeKey // our node privkey - addrBook AddrBook - persistentPeers map[ID]bool + config *config.P2PConfig + reactors map[string]Reactor + chDescs []*conn.ChannelDescriptor + reactorsByCh map[byte]Reactor + peers *PeerSet + dialing *cmn.CMap + reconnecting *cmn.CMap + nodeInfo NodeInfo // our node info + nodeKey *NodeKey // our node privkey + addrBook AddrBook transport Transport @@ -121,7 +120,7 @@ func NewSwitch( // Ensure we have a completely undeterministic PRNG. sw.rng = cmn.NewRand() - sw.initPersistentPeers() + sw.BaseService = *cmn.NewBaseService(nil, "P2P Switch", sw) for _, option := range options { @@ -198,19 +197,18 @@ func (sw *Switch) SetNodeKey(nodeKey *NodeKey) { sw.nodeKey = nodeKey } -func (sw *Switch) initPersistentPeers() { - sw.persistentPeers = make(map[ID]bool, 0) - if sw.config.PersistentPeers != "" { - peers := cmn.SplitAndTrim(sw.config.PersistentPeers, ",", " ") - netAddrs, _ := NewNetAddressStrings(peers) - for _, addr := range netAddrs { - sw.persistentPeers[addr.ID] = true +func (sw *Switch) IsPersistent(peer Peer) bool { + if sw.config.PersistentPeers == "" { + return false + } + peers := cmn.SplitAndTrim(sw.config.PersistentPeers, ",", " ") + netAddrs, _ := NewNetAddressStrings(peers) + for _, addr := range netAddrs { + if addr.ID == peer.ID() { + return true } } -} - -func (sw *Switch) IsPersistent(peer Peer) bool { - return sw.persistentPeers[peer.ID()] + return false } //--------------------------------------------------------------------- diff --git a/p2p/switch_test.go b/p2p/switch_test.go index 5b49b05a9..5e830ccba 100644 --- a/p2p/switch_test.go +++ b/p2p/switch_test.go @@ -405,7 +405,6 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { defer rp.Stop() addr := *rp.Addr() sw.config.PersistentPeers = addr.String() - sw.initPersistentPeers() p, err := sw.transport.Dial(addr, peerConfig{ chDescs: sw.chDescs, @@ -449,7 +448,6 @@ func TestSwitchReconnectsToPersistentPeer(t *testing.T) { conf := config.DefaultP2PConfig() conf.TestDialFail = true sw.config.PersistentPeers = fmt.Sprintf("%s,%s", sw.config.PersistentPeers, rp.Addr().String()) - sw.initPersistentPeers() err = sw.addOutboundPeerWithConfig(rp.Addr(), conf, true) require.NotNil(err) diff --git a/rpc/core/pipe.go b/rpc/core/pipe.go index 43eabf53b..6f1579b6d 100644 --- a/rpc/core/pipe.go +++ b/rpc/core/pipe.go @@ -75,7 +75,6 @@ var ( consensusReactor *consensus.ConsensusReactor eventBus *types.EventBus // thread safe mempool *mempl.Mempool - indexerHub *sm.IndexHub logger log.Logger @@ -134,10 +133,6 @@ func SetBlockIndexer(indexer blockindex.BlockIndexer) { blockIndexer = indexer } -func SetIndexHub(hub *sm.IndexHub) { - indexerHub = hub -} - func SetConsensusReactor(conR *consensus.ConsensusReactor) { consensusReactor = conR } diff --git a/rpc/core/status.go b/rpc/core/status.go index f2f0ddb82..f2e0624d4 100644 --- a/rpc/core/status.go +++ b/rpc/core/status.go @@ -58,8 +58,7 @@ import ( // "latest_app_hash": "0000000000000000", // "latest_block_height": "18", // "latest_block_time": "2018-09-17T11:42:19.149920551Z", -// "catching_up": false, -// "index_height": "18" +// "catching_up": false // }, // "validator_info": { // "address": "D9F56456D7C5793815D0E9AF07C3A355D0FC64FD", @@ -107,7 +106,6 @@ func Status(ctx *rpctypes.Context) (*ctypes.ResultStatus, error) { LatestBlockHeight: latestHeight, LatestBlockTime: latestBlockTime, CatchingUp: consensusReactor.FastSync(), - IndexHeight: indexerHub.GetHeight(), }, ValidatorInfo: ctypes.ValidatorInfo{ Address: pubKey.Address(), diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index 075fa957a..74457b38a 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -63,7 +63,6 @@ type SyncInfo struct { LatestBlockHeight int64 `json:"latest_block_height"` LatestBlockTime time.Time `json:"latest_block_time"` CatchingUp bool `json:"catching_up"` - IndexHeight int64 `json:"index_height"` } // Info about the node's validator diff --git a/snapshot/reactor.go b/snapshot/reactor.go index b5f683dd0..5fa8da7f3 100644 --- a/snapshot/reactor.go +++ b/snapshot/reactor.go @@ -27,10 +27,10 @@ const ( StateSyncChannel = byte(0x36) // ====== move to config ====== - stateSyncPriority = 10 // channel priority of state sync - tryManifestFinalizeSeconds = 10 // how often to check whether we collect enough manifests - maxTriedManifestFinalist = 3 // max round of try finalize manifest - leastPeersToSync = 3 // how many peers with same manifest hash before we can start state sync + stateSyncPriority = 10 // channel priority of state sync + tryManifestFinalizeSeconds = 10 // how often to check whether we collect enough manifests + maxTriedManifestFinalist = 3 // max round of try finalize manifest + leastPeersToSync = 3 // how many peers with same manifest hash before we can start state sync // NOTE: keep up to date with bcChunkResponseMessage // TODO: REVIEW before final merge @@ -39,7 +39,7 @@ const ( maxStateMsgSize = types.MaxStateSizeBytes + bcStateResponseMessagePrefixSize + bcStateResponseMessageFieldKeySize - maxInFlightRequesters = 600 + maxInFlightRequesters = 600 maxInflightRequestPerPeer = 20 @@ -88,11 +88,11 @@ func NewStateReactor(stateDB dbm.DB, app proxy.AppConnState, config *cfg.Config) } bcSR := &StateReactor{ - config: config, - pool: pool, - stateSync: config.StateSyncHeight, - requestsCh: requestsCh, - errorsCh: errorsCh, + config: config, + pool: pool, + stateSync: config.StateSyncHeight, + requestsCh: requestsCh, + errorsCh: errorsCh, } bcSR.BaseReactor = *p2p.NewBaseReactor("StateSyncReactor", bcSR) return bcSR @@ -290,7 +290,7 @@ func (bcSR *StateReactor) poolRoutine() { go func() { for { select { - case <-bcSR.pool.Quit(): + case <- bcSR.pool.Quit(): return case <-bcSR.Quit(): @@ -332,7 +332,7 @@ func (bcSR *StateReactor) poolRoutine() { // always broadcast state status request to get more potential trusted peers even pool already inited bcSR.BroadcastStateStatusRequest() - case <-bcSR.pool.Quit(): + case <- bcSR.pool.Quit(): return case <-bcSR.Quit(): @@ -382,7 +382,7 @@ func (bcSR *StateReactor) decodeMsg(bz []byte) (msg BlockchainStateMessage, err type bcChunkRequestMessage struct { Height int64 - Hash abci.SHA256Sum + Hash abci.SHA256Sum } func (m *bcChunkRequestMessage) String() string { @@ -391,7 +391,7 @@ func (m *bcChunkRequestMessage) String() string { type bcNoChunkResponseMessage struct { Height int64 - Hash abci.SHA256Sum + Hash abci.SHA256Sum } func (m *bcNoChunkResponseMessage) String() string { diff --git a/state/blockindex/indexer_service.go b/state/blockindex/indexer_service.go index f41768e0f..2a4b3bf81 100644 --- a/state/blockindex/indexer_service.go +++ b/state/blockindex/indexer_service.go @@ -19,8 +19,6 @@ type IndexerService struct { idr BlockIndexer eventBus *types.EventBus - - onIndex func(int64) } // NewIndexerService returns a new service instance. @@ -30,10 +28,6 @@ func NewIndexerService(idr BlockIndexer, eventBus *types.EventBus) *IndexerServi return is } -func (is *IndexerService) SetOnIndex(callback func(int64)) { - is.onIndex = callback -} - // OnStart implements cmn.Service by subscribing for blocks and indexing them by hash. func (is *IndexerService) OnStart() error { blockHeadersSub, err := is.eventBus.SubscribeUnbuffered(context.Background(), subscriber, types.EventQueryNewBlockHeader) @@ -51,9 +45,6 @@ func (is *IndexerService) OnStart() error { } else { is.Logger.Info("Indexed block", "height", header.Height, "hash", header.LastBlockID.Hash) } - if is.onIndex != nil { - is.onIndex(header.Height) - } } }() return nil diff --git a/state/execution.go b/state/execution.go index 07e76c9ad..78ce171fb 100644 --- a/state/execution.go +++ b/state/execution.go @@ -54,13 +54,13 @@ func BlockExecutorWithMetrics(metrics *Metrics) BlockExecutorOption { func NewBlockExecutor(db dbm.DB, logger log.Logger, proxyApp proxy.AppConnConsensus, mempool Mempool, evpool EvidencePool, withAppState bool, options ...BlockExecutorOption) *BlockExecutor { res := &BlockExecutor{ - db: db, - proxyApp: proxyApp, - eventBus: types.NopEventBus{}, - mempool: mempool, - evpool: evpool, - logger: logger, - metrics: NopMetrics(), + db: db, + proxyApp: proxyApp, + eventBus: types.NopEventBus{}, + mempool: mempool, + evpool: evpool, + logger: logger, + metrics: NopMetrics(), withAppState: withAppState, } diff --git a/state/index.go b/state/index.go deleted file mode 100644 index 82b4b701e..000000000 --- a/state/index.go +++ /dev/null @@ -1,181 +0,0 @@ -package state - -import ( - "sync" - - cmn "github.com/tendermint/tendermint/libs/common" - dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/types" - abci "github.com/tendermint/tendermint/abci/types" - -) - -const ( - MaxIndexLag = 100 -) - -var IndexHeightKey = []byte("IndexHeightKey") - -type IndexService interface { - SetOnIndex(callback func(int64)) -} - -type IndexHub struct { - cmn.BaseService - mtx sync.Mutex - - stateHeight int64 - expectHeight int64 - - // the total registered index service - numIdxSvc int - indexTaskCounter map[int64]int - indexTaskEvents chan int64 - - stateDB dbm.DB - blockStore BlockStore - eventBus types.BlockEventPublisher - - metrics *Metrics -} - -func NewIndexHub(initialHeight int64, stateDB dbm.DB, blockStore BlockStore, eventBus types.BlockEventPublisher, options ...IndexHubOption) *IndexHub { - ih := &IndexHub{ - stateHeight: initialHeight, - indexTaskCounter: make(map[int64]int, 0), - indexTaskEvents: make(chan int64, MaxIndexLag), - stateDB: stateDB, - blockStore: blockStore, - eventBus: eventBus, - metrics: NopMetrics(), - } - indexedHeight := ih.GetIndexedHeight() - if indexedHeight < 0 { - // no indexedHeight found, will do no recover - ih.expectHeight = ih.stateHeight + 1 - } else { - ih.expectHeight = indexedHeight + 1 - } - for _, option := range options { - option(ih) - } - ih.BaseService = *cmn.NewBaseService(nil, "indexHub", ih) - return ih -} - -type IndexHubOption func(*IndexHub) - -func IndexHubWithMetrics(metrics *Metrics) IndexHubOption { - return func(ih *IndexHub) { - ih.metrics = metrics - } -} - -func (ih *IndexHub) OnStart() error { - // start listen routine before recovering. - go ih.commitIndexRoutine() - ih.recoverIndex() - return nil -} - -func (ih *IndexHub) recoverIndex() { - for h := ih.expectHeight; h <= ih.stateHeight; h++ { - ih.Logger.Info("try to recover index", "height", h) - block := ih.blockStore.LoadBlock(h) - if block == nil { - ih.Logger.Error("index skip since the the block is missing", "height", h) - } else { - abciResponses, err := LoadABCIResponses(ih.stateDB, h) - if err != nil { - ih.Logger.Error("failed to load ABCIResponse, will use default") - abciResponses = NewABCIResponses(block) - abciResponses.EndBlock = &abci.ResponseEndBlock{} - abciResponses.BeginBlock = &abci.ResponseBeginBlock{} - } - abciValUpdates := abciResponses.EndBlock.ValidatorUpdates - validatorUpdates, err := types.PB2TM.ValidatorUpdates(abciValUpdates) - if err != nil { - ih.Logger.Error("failed to load validatorUpdates, will use nil by default") - } - fireEvents(ih.Logger, ih.eventBus, block, abciResponses, validatorUpdates) - } - } -} - -func (ih *IndexHub) commitIndexRoutine() { - for { - select { - case <-ih.Quit(): - return - case h := <-ih.indexTaskEvents: - ih.Logger.Info("finish index", "height", h) - ih.SetIndexedHeight(h) - ih.metrics.IndexHeight.Set(float64(h)) - } - } -} - -func (ih *IndexHub) RegisterIndexSvc(idx IndexService) { - ih.mtx.Lock() - defer ih.mtx.Unlock() - if ih.IsRunning() { - panic("can't RegisterIndexSvc when IndexHub is running") - } - idx.SetOnIndex(ih.CountDownAt) - ih.numIdxSvc++ -} - -// `CountDownAt` is a callback in index service, keep it simple and fast. -func (ih *IndexHub) CountDownAt(height int64) { - ih.mtx.Lock() - defer ih.mtx.Unlock() - count, exist := ih.indexTaskCounter[height] - if exist { - count = count - 1 - } else { - count = ih.numIdxSvc - 1 - } - // The higher block won't finish index before lower one. - if count == 0 && height == ih.expectHeight { - if exist { - delete(ih.indexTaskCounter, height) - } - ih.expectHeight = ih.expectHeight + 1 - ih.indexTaskEvents <- height - } else { - ih.indexTaskCounter[height] = count - } -} - -// set and get won't happen in the same time, won't lock -func (ih *IndexHub) SetIndexedHeight(h int64) { - rawHeight, err := cdc.MarshalBinaryBare(h) - if err != nil { - ih.Logger.Error("failed to MarshalBinaryBare for indexed height", "error", err, "height", h) - } else { - ih.stateDB.Set(IndexHeightKey, rawHeight) - } -} - -// if never store `IndexHeightKey` in index db, will return -1. -func (ih *IndexHub) GetIndexedHeight() int64 { - rawHeight := ih.stateDB.Get(IndexHeightKey) - if rawHeight == nil { - return -1 - } else { - var height int64 - err := cdc.UnmarshalBinaryBare(rawHeight, &height) - if err != nil { - // should not happen - panic(err) - } - return height - } -} - -// get indexed height from memory to save time for RPC -func (ih *IndexHub) GetHeight() int64 { - ih.mtx.Lock() - defer ih.mtx.Unlock() - return ih.expectHeight - 1 -} diff --git a/state/index_test.go b/state/index_test.go deleted file mode 100644 index 041f8a98f..000000000 --- a/state/index_test.go +++ /dev/null @@ -1,90 +0,0 @@ -package state - -import ( - "math/rand" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - abci "github.com/tendermint/tendermint/abci/types" - "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/state/blockindex" - kv2 "github.com/tendermint/tendermint/state/blockindex/kv" - "github.com/tendermint/tendermint/state/txindex" - "github.com/tendermint/tendermint/state/txindex/kv" - "github.com/tendermint/tendermint/types" -) - -func TestSetHeight(t *testing.T) { - - indexDb := db.NewMemDB() - indexHub := NewIndexHub(0, indexDb, nil, nil) - indexHub.SetLogger(log.TestingLogger()) - - realHeightAtFirst := indexHub.GetIndexedHeight() - assert.Equal(t, int64(-1), realHeightAtFirst) - height := int64(1024) - indexHub.SetIndexedHeight(height) - realHeight := indexHub.GetIndexedHeight() - assert.Equal(t, height, realHeight) -} - -func TestCountDown(t *testing.T) { - // event bus - eventBus := types.NewEventBus() - eventBus.SetLogger(log.TestingLogger()) - err := eventBus.Start() - require.NoError(t, err) - defer eventBus.Stop() - - indexDb := db.NewMemDB() - - // start tx index - txIndexer := kv.NewTxIndex(indexDb, kv.IndexAllTags()) - txIndexSvc := txindex.NewIndexerService(txIndexer, eventBus) - txIndexSvc.SetLogger(log.TestingLogger()) - err = txIndexSvc.Start() - require.NoError(t, err) - defer txIndexSvc.Stop() - - // start block index - blockIndexer := kv2.NewBlockIndex(indexDb) - blockIndexSvc := blockindex.NewIndexerService(blockIndexer, eventBus) - blockIndexSvc.SetLogger(log.TestingLogger()) - err = blockIndexSvc.Start() - require.NoError(t, err) - defer blockIndexSvc.Stop() - - // start index hub - indexHub := NewIndexHub(0, indexDb, nil, eventBus) - indexHub.SetLogger(log.TestingLogger()) - indexHub.RegisterIndexSvc(txIndexSvc) - indexHub.RegisterIndexSvc(blockIndexSvc) - err = indexHub.Start() - assert.NoError(t, err) - - // publish block with txs - for h := int64(1); h < 10; h++ { - numTxs := rand.Int63n(5) - eventBus.PublishEventNewBlockHeader(types.EventDataNewBlockHeader{ - Header: types.Header{Height: h, NumTxs: numTxs}, - }) - for i := int64(0); i < numTxs; i++ { - txResult := &types.TxResult{ - Height: h, - Index: uint32(i), - Tx: types.Tx("foo"), - Result: abci.ResponseDeliverTx{Code: 0}, - } - eventBus.PublishEventTx(types.EventDataTx{*txResult}) - } - // In test case, 100ms is far enough for index - time.Sleep(100 * time.Millisecond) - assert.Equal(t, int64(h), indexHub.GetIndexedHeight()) - // test no memory leak - assert.Equal(t, len(indexHub.indexTaskCounter), 0) - } -} diff --git a/state/metrics.go b/state/metrics.go index 23f01225e..bcd713f5f 100644 --- a/state/metrics.go +++ b/state/metrics.go @@ -17,8 +17,6 @@ const ( type Metrics struct { // Time between BeginBlock and EndBlock. BlockProcessingTime metrics.Histogram - // The latest height of block that have been indexed - IndexHeight metrics.Gauge } // PrometheusMetrics returns Metrics build using Prometheus client library. @@ -37,12 +35,6 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { Help: "Time between BeginBlock and EndBlock in ms.", Buckets: stdprometheus.LinearBuckets(1, 10, 10), }, labels).With(labelsAndValues...), - IndexHeight: prometheus.NewGaugeFrom(stdprometheus.GaugeOpts{ - Namespace: namespace, - Subsystem: MetricsSubsystem, - Name: "index_height", - Help: "The latest height of block that have been indexed", - }, append(labels)).With(labelsAndValues...), } } @@ -50,6 +42,5 @@ func PrometheusMetrics(namespace string, labelsAndValues ...string) *Metrics { func NopMetrics() *Metrics { return &Metrics{ BlockProcessingTime: discard.NewHistogram(), - IndexHeight: discard.NewGauge(), } } diff --git a/state/store.go b/state/store.go index 5938b202e..b9cf119ab 100644 --- a/state/store.go +++ b/state/store.go @@ -22,7 +22,7 @@ const ( const latestStateToKeep int64 = 1 << 20 func calcStateKey(height int64) []byte { - return []byte(fmt.Sprintf("stateKey:%v", height%latestStateToKeep)) + return []byte(fmt.Sprintf("stateKey:%v", height % latestStateToKeep)) } func calcValidatorsKey(height int64) []byte { diff --git a/state/txindex/indexer_service.go b/state/txindex/indexer_service.go index de9d83688..03fa9c03e 100644 --- a/state/txindex/indexer_service.go +++ b/state/txindex/indexer_service.go @@ -19,8 +19,6 @@ type IndexerService struct { idr TxIndexer eventBus *types.EventBus - - onIndex func(int64) } // NewIndexerService returns a new service instance. @@ -30,10 +28,6 @@ func NewIndexerService(idr TxIndexer, eventBus *types.EventBus) *IndexerService return is } -func (is *IndexerService) SetOnIndex(callback func(int64)) { - is.onIndex = callback -} - // OnStart implements cmn.Service by subscribing for all transactions // and indexing them by tags. func (is *IndexerService) OnStart() error { @@ -71,9 +65,6 @@ func (is *IndexerService) OnStart() error { } else { is.Logger.Info("Indexed txs for block", "height", header.Height) } - if is.onIndex != nil { - is.onIndex(header.Height) - } } }() return nil diff --git a/types/event_bus.go b/types/event_bus.go index 9a9d34b3e..da959090f 100644 --- a/types/event_bus.go +++ b/types/event_bus.go @@ -189,10 +189,6 @@ func (b *EventBus) PublishEventCompleteProposal(data EventDataCompleteProposal) return b.Publish(EventCompleteProposal, data) } -func (b *EventBus) PublishEventMajorPrecommits(data EventDataMajorPrecommits) error { - return b.Publish(EventMajorPrecommits, data) -} - func (b *EventBus) PublishEventPolka(data EventDataRoundState) error { return b.Publish(EventPolka, data) } diff --git a/types/events.go b/types/events.go index 297d476f6..b65ea3832 100644 --- a/types/events.go +++ b/types/events.go @@ -34,7 +34,6 @@ const ( EventTimeoutWait = "TimeoutWait" EventUnlock = "Unlock" EventValidBlock = "ValidBlock" - EventMajorPrecommits = "MajorPrecommits" EventVote = "Vote" ) @@ -109,15 +108,6 @@ type EventDataCompleteProposal struct { Step string `json:"step"` BlockID BlockID `json:"block_id"` - Block Block `json:"block"` -} - -type EventDataMajorPrecommits struct { - Height int64 `json:"height"` - Round int `json:"round"` - Step string `json:"step"` - - Votes VoteSet `json:"votes"` } type EventDataVote struct { @@ -147,7 +137,6 @@ const ( var ( EventQueryCompleteProposal = QueryForEvent(EventCompleteProposal) - EventQueryMajorPrecommits = QueryForEvent(EventMajorPrecommits) EventQueryLock = QueryForEvent(EventLock) EventQueryNewBlock = QueryForEvent(EventNewBlock) EventQueryNewBlockHeader = QueryForEvent(EventNewBlockHeader) diff --git a/types/vote_set.go b/types/vote_set.go index 81b6a0c8c..1cd0f2281 100644 --- a/types/vote_set.go +++ b/types/vote_set.go @@ -119,21 +119,6 @@ func (voteSet *VoteSet) Size() int { return voteSet.valSet.Size() } -func (voteSet *VoteSet) Copy() *VoteSet { - if voteSet == nil { - return nil - } - return &VoteSet{ - chainID: voteSet.chainID, - height: voteSet.height, - round: voteSet.round, - type_: voteSet.type_, - valSet: voteSet.valSet, - votes: voteSet.votes, - maj23: voteSet.maj23, - } -} - // Returns added=true if vote is valid and new. // Otherwise returns err=ErrVote[ // UnexpectedStep | InvalidIndex | InvalidAddress | From 3f12c8d242544767f99ea5d9dfaf9857e1fa5a53 Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 2 Aug 2019 08:53:52 +0200 Subject: [PATCH 165/211] gocritic (2/2) (#3864) Refs #3262 --- .golangci.yml | 1 - abci/cmd/abci-cli/abci-cli.go | 12 +++++++----- abci/example/kvstore/kvstore_test.go | 2 +- blockchain/v0/pool.go | 7 ++++--- consensus/replay.go | 26 +++++++++++++++----------- consensus/replay_test.go | 3 +-- consensus/state.go | 14 ++++++++------ crypto/merkle/simple_proof.go | 7 ++++--- go.mod | 3 +++ go.sum | 9 +++++++++ libs/autofile/group.go | 7 ++++--- libs/common/async_test.go | 14 ++++++++------ libs/common/errors.go | 2 +- libs/log/tmfmt_logger.go | 9 +++++---- mempool/clist_mempool_test.go | 2 +- node/node.go | 5 +++-- p2p/fuzz.go | 7 ++++--- p2p/netaddress.go | 27 +++++++++++++++------------ p2p/node_info_test.go | 2 +- p2p/switch.go | 3 +-- rpc/lib/client/http_client.go | 7 ++++--- rpc/lib/server/handlers.go | 7 ++++--- state/execution.go | 3 +-- state/txindex/kv/kv.go | 10 +++++----- tools/tm-monitor/monitor/network.go | 7 ++++--- types/validator.go | 14 ++++++++------ 26 files changed, 121 insertions(+), 89 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b07ec3a46..17d575316 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -15,7 +15,6 @@ linters: - nakedret - lll - gochecknoglobals - - gocritic - gochecknoinits - scopelint - stylecheck diff --git a/abci/cmd/abci-cli/abci-cli.go b/abci/cmd/abci-cli/abci-cli.go index 5f0685107..31721b21b 100644 --- a/abci/cmd/abci-cli/abci-cli.go +++ b/abci/cmd/abci-cli/abci-cli.go @@ -332,16 +332,18 @@ func cmdTest(cmd *cobra.Command, args []string) error { func cmdBatch(cmd *cobra.Command, args []string) error { bufReader := bufio.NewReader(os.Stdin) +LOOP: for { line, more, err := bufReader.ReadLine() - if more { + switch { + case more: return errors.New("Input line is too long") - } else if err == io.EOF { - break - } else if len(line) == 0 { + case err == io.EOF: + break LOOP + case len(line) == 0: continue - } else if err != nil { + case err != nil: return err } diff --git a/abci/example/kvstore/kvstore_test.go b/abci/example/kvstore/kvstore_test.go index 1649d3e84..80b07ff5a 100644 --- a/abci/example/kvstore/kvstore_test.go +++ b/abci/example/kvstore/kvstore_test.go @@ -148,7 +148,7 @@ func TestValUpdates(t *testing.T) { makeApplyBlock(t, kvstore, 2, diff, tx1, tx2, tx3) - vals1 = append(vals[:nInit-2], vals[nInit+1]) + vals1 = append(vals[:nInit-2], vals[nInit+1]) // nolint: gocritic vals2 = kvstore.Validators() valsEqual(t, vals1, vals2) diff --git a/blockchain/v0/pool.go b/blockchain/v0/pool.go index 6b8638ee2..46b4b915a 100644 --- a/blockchain/v0/pool.go +++ b/blockchain/v0/pool.go @@ -116,17 +116,18 @@ func (pool *BlockPool) makeRequestersRoutine() { } _, numPending, lenRequesters := pool.GetStatus() - if numPending >= maxPendingRequests { + switch { + case numPending >= maxPendingRequests: // sleep for a bit. time.Sleep(requestIntervalMS * time.Millisecond) // check for timed out peers pool.removeTimedoutPeers() - } else if lenRequesters >= maxTotalRequesters { + case lenRequesters >= maxTotalRequesters: // sleep for a bit. time.Sleep(requestIntervalMS * time.Millisecond) // check for timed out peers pool.removeTimedoutPeers() - } else { + default: // request for more blocks. pool.makeNextRequester() } diff --git a/consensus/replay.go b/consensus/replay.go index 0c208efef..8fc7a723c 100644 --- a/consensus/replay.go +++ b/consensus/replay.go @@ -141,14 +141,16 @@ func (cs *ConsensusState) catchupReplay(csHeight int64) error { var msg *TimedWALMessage dec := WALDecoder{gr} +LOOP: for { msg, err = dec.Decode() - if err == io.EOF { - break - } else if IsDataCorruptionError(err) { + switch { + case err == io.EOF: + break LOOP + case IsDataCorruptionError(err): cs.Logger.Error("data has been corrupted in last height of consensus WAL", "err", err, "height", csHeight) return err - } else if err != nil { + case err != nil: return err } @@ -335,19 +337,20 @@ func (h *Handshaker) ReplayBlocks( } // First handle edge cases and constraints on the storeBlockHeight. - if storeBlockHeight == 0 { + switch { + case storeBlockHeight == 0: assertAppHashEqualsOneFromState(appHash, state, !h.withAppStat) return appHash, nil - } else if storeBlockHeight < appBlockHeight { + case storeBlockHeight < appBlockHeight: // the app should never be ahead of the store (but this is under app's control) return appHash, sm.ErrAppBlockHeightTooHigh{CoreHeight: storeBlockHeight, AppHeight: appBlockHeight} - } else if storeBlockHeight < stateBlockHeight { + case storeBlockHeight < stateBlockHeight: // the state should never be ahead of the store (this is under tendermint's control) panic(fmt.Sprintf("StateBlockHeight (%d) > StoreBlockHeight (%d)", stateBlockHeight, storeBlockHeight)) - } else if storeBlockHeight > stateBlockHeight+1 { + case storeBlockHeight > stateBlockHeight+1: // store should be at most one ahead of the state (this is under tendermint's control) panic(fmt.Sprintf("StoreBlockHeight (%d) > StateBlockHeight + 1 (%d)", storeBlockHeight, stateBlockHeight+1)) } @@ -371,12 +374,13 @@ func (h *Handshaker) ReplayBlocks( } else if storeBlockHeight == stateBlockHeight+1 { // We saved the block in the store but haven't updated the state, // so we'll need to replay a block using the WAL. - if appBlockHeight < stateBlockHeight { + switch { + 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) - } else if appBlockHeight == stateBlockHeight { + case appBlockHeight == stateBlockHeight: // We haven't run Commit (both the state and app are one block behind), // so replayBlock with the real app. // NOTE: We could instead use the cs.WAL on cs.Start, @@ -385,7 +389,7 @@ func (h *Handshaker) ReplayBlocks( state, err = h.replayBlock(state, storeBlockHeight, proxyApp.Consensus()) return state.AppHash, err - } else if appBlockHeight == storeBlockHeight { + case appBlockHeight == storeBlockHeight: // We ran Commit, but didn't save the state, so replayBlock with mock app. abciResponses, err := sm.LoadABCIResponses(h.stateDB, storeBlockHeight) if err != nil { diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 4ff99b991..3762f8816 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -545,8 +545,7 @@ func TestMockProxyApp(t *testing.T) { abciRes.DeliverTx = make([]*abci.ResponseDeliverTx, len(loadedAbciRes.DeliverTx)) // Execute transactions and get hash. proxyCb := func(req *abci.Request, res *abci.Response) { - switch r := res.Value.(type) { - case *abci.Response_DeliverTx: + if r, ok := res.Value.(*abci.Response_DeliverTx); ok { // TODO: make use of res.Log // TODO: make use of this info // Blocks may include invalid txs. diff --git a/consensus/state.go b/consensus/state.go index 30179d313..4756fe0e5 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -952,14 +952,15 @@ func (cs *ConsensusState) isProposalComplete() bool { // NOTE: keep it side-effect free for clarity. func (cs *ConsensusState) createProposalBlock() (block *types.Block, blockParts *types.PartSet) { var commit *types.Commit - if cs.Height == 1 { + switch { + case cs.Height == 1: // We're creating a proposal for the first block. // The commit is empty, but not nil. commit = types.NewCommit(types.BlockID{}, nil) - } else if cs.LastCommit.HasTwoThirdsMajority() { + case cs.LastCommit.HasTwoThirdsMajority(): // Make the commit from LastCommit commit = cs.LastCommit.MakeCommit() - } else { + default: // This shouldn't happen. cs.Logger.Error("enterPropose: Cannot propose anything: No commit for the previous block.") return @@ -1628,17 +1629,18 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, } // If +2/3 prevotes for *anything* for future round: - if cs.Round < vote.Round && prevotes.HasTwoThirdsAny() { + 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) - } else if cs.Round == vote.Round && cstypes.RoundStepPrevote <= cs.Step { // current 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) } else if prevotes.HasTwoThirdsAny() { cs.enterPrevoteWait(height, vote.Round) } - } else if cs.Proposal != nil && 0 <= cs.Proposal.POLRound && cs.Proposal.POLRound == 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) diff --git a/crypto/merkle/simple_proof.go b/crypto/merkle/simple_proof.go index f01dcdca1..d3be5d7ec 100644 --- a/crypto/merkle/simple_proof.go +++ b/crypto/merkle/simple_proof.go @@ -162,11 +162,12 @@ func (spn *SimpleProofNode) FlattenAunts() [][]byte { // Nonrecursive impl. innerHashes := [][]byte{} for spn != nil { - if spn.Left != nil { + switch { + case spn.Left != nil: innerHashes = append(innerHashes, spn.Left.Hash) - } else if spn.Right != nil { + case spn.Right != nil: innerHashes = append(innerHashes, spn.Right.Hash) - } else { + default: break } spn = spn.Parent diff --git a/go.mod b/go.mod index 9fa4f7b3a..5e37b6c59 100644 --- a/go.mod +++ b/go.mod @@ -16,8 +16,10 @@ require ( github.com/golang/protobuf v1.3.2 github.com/golang/snappy v0.0.1 github.com/google/gofuzz v1.0.0 // indirect + github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70 // indirect github.com/gorilla/websocket v1.2.0 github.com/hashicorp/hcl v1.0.0 // indirect + github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmhodges/levigo v1.0.0 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect @@ -43,6 +45,7 @@ require ( github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 github.com/tendermint/go-amino v0.14.1 github.com/tendermint/tm-db v0.1.1 + golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c // indirect golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/net v0.0.0-20190909003024-a7b16738d86b google.golang.org/grpc v1.23.0 diff --git a/go.sum b/go.sum index dbbaa8931..d29c060fd 100644 --- a/go.sum +++ b/go.sum @@ -31,9 +31,15 @@ github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70 h1:XTnP8fJpa4Kvpw2qARB4KS9izqxPS0Sd92cDlY3uk+w= +github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= +github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= @@ -73,6 +79,8 @@ github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYM github.com/tendermint/tm-cmn v0.1.1 h1:sSbm79PeQQdmbgiPjFDRT70cSXBVFVmxn5/sQrCNrUI= github.com/tendermint/tm-db v0.1.1/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c h1:Rx/HTKi09myZ25t1SOlDHmHOy/mKxNAcu0hP1oPX9qM= +golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -110,3 +118,4 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/libs/autofile/group.go b/libs/autofile/group.go index ce73466e4..7cc345478 100644 --- a/libs/autofile/group.go +++ b/libs/autofile/group.go @@ -472,7 +472,8 @@ func (gr *GroupReader) Read(p []byte) (n int, err error) { for { nn, err = gr.curReader.Read(p[n:]) n += nn - if err == io.EOF { + switch { + case err == io.EOF: if n >= lenP { return n, nil } @@ -480,9 +481,9 @@ func (gr *GroupReader) Read(p []byte) (n int, err error) { if err1 := gr.openFile(gr.curIndex + 1); err1 != nil { return n, err1 } - } else if err != nil { + case err != nil: return n, err - } else if nn == 0 { // empty file + case nn == 0: // empty file return n, err } } diff --git a/libs/common/async_test.go b/libs/common/async_test.go index c19ffc86f..9ac5ffe3f 100644 --- a/libs/common/async_test.go +++ b/libs/common/async_test.go @@ -31,13 +31,14 @@ func TestParallel(t *testing.T) { var failedTasks int for i := 0; i < len(tasks); i++ { taskResult, ok := trs.LatestResult(i) - if !ok { + switch { + case !ok: assert.Fail(t, "Task #%v did not complete.", i) failedTasks++ - } else if taskResult.Error != nil { + case taskResult.Error != nil: assert.Fail(t, "Task should not have errored but got %v", taskResult.Error) failedTasks++ - } else if !assert.Equal(t, -1*i, taskResult.Value.(int)) { + case !assert.Equal(t, -1*i, taskResult.Value.(int)): assert.Fail(t, "Task should have returned %v but got %v", -1*i, taskResult.Value.(int)) failedTasks++ } @@ -133,11 +134,12 @@ func checkResult(t *testing.T, taskResultSet *TaskResultSet, index int, val inte taskName := fmt.Sprintf("Task #%v", index) assert.True(t, ok, "TaskResultCh unexpectedly closed for %v", taskName) assert.Equal(t, val, taskResult.Value, taskName) - if err != nil { + switch { + case err != nil: assert.Equal(t, err, taskResult.Error, taskName) - } else if pnk != nil { + case pnk != nil: assert.Equal(t, pnk, taskResult.Error.(Error).Data(), taskName) - } else { + default: assert.Nil(t, taskResult.Error, taskName) } } diff --git a/libs/common/errors.go b/libs/common/errors.go index 24af84267..aacfbe274 100644 --- a/libs/common/errors.go +++ b/libs/common/errors.go @@ -9,7 +9,7 @@ import ( // Convenience method. func ErrorWrap(cause interface{}, format string, args ...interface{}) Error { - if causeCmnError, ok := cause.(*cmnError); ok { + if causeCmnError, ok := cause.(*cmnError); ok { //nolint:gocritic msg := fmt.Sprintf(format, args...) return causeCmnError.Stacktrace().Trace(1, msg) } else if cause == nil { diff --git a/libs/log/tmfmt_logger.go b/libs/log/tmfmt_logger.go index d841263ea..d57f9558e 100644 --- a/libs/log/tmfmt_logger.go +++ b/libs/log/tmfmt_logger.go @@ -60,9 +60,10 @@ func (l tmfmtLogger) Log(keyvals ...interface{}) error { for i := 0; i < len(keyvals)-1; i += 2 { // Extract level - if keyvals[i] == kitlevel.Key() { + switch keyvals[i] { + case kitlevel.Key(): excludeIndexes = append(excludeIndexes, i) - switch keyvals[i+1].(type) { + switch keyvals[i+1].(type) { // nolint:gocritic case string: lvl = keyvals[i+1].(string) case kitlevel.Value: @@ -71,11 +72,11 @@ func (l tmfmtLogger) Log(keyvals ...interface{}) error { panic(fmt.Sprintf("level value of unknown type %T", keyvals[i+1])) } // and message - } else if keyvals[i] == msgKey { + case msgKey: excludeIndexes = append(excludeIndexes, i) msg = keyvals[i+1].(string) // and module (could be multiple keyvals; if such case last keyvalue wins) - } else if keyvals[i] == moduleKey { + case moduleKey: excludeIndexes = append(excludeIndexes, i) module = keyvals[i+1].(string) } diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index 0abc9233c..c8d5a93de 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -245,7 +245,7 @@ func TestTxsAvailable(t *testing.T) { ensureNoFire(t, mempool.TxsAvailable(), timeoutMS) // now call update with all the txs. it should not fire as there are no txs left - committedTxs = append(txs, moreTxs...) + committedTxs = append(txs, moreTxs...) //nolint: gocritic if err := mempool.Update(2, committedTxs, abciResponses(len(committedTxs), abci.CodeTypeOK), nil, nil); err != nil { t.Error(err) } diff --git a/node/node.go b/node/node.go index 1d68751a8..548914ffe 100644 --- a/node/node.go +++ b/node/node.go @@ -254,9 +254,10 @@ func createAndStartIndexerService(config *cfg.Config, dbProvider DBProvider, return } indexOptions := make([]func(index *kv.TxIndex), 0) - if config.TxIndex.IndexTags != "" { + switch { + case config.TxIndex.IndexTags != "": indexOptions = append(indexOptions, kv.IndexTags(splitAndTrimEmpty(config.TxIndex.IndexTags, ",", " "))) - } else if config.TxIndex.IndexAllTags { + case config.TxIndex.IndexAllTags: indexOptions = append(indexOptions, kv.IndexAllTags()) } if config.TxIndex.EnableRangeQuery { diff --git a/p2p/fuzz.go b/p2p/fuzz.go index 80e4fed6a..135155d86 100644 --- a/p2p/fuzz.go +++ b/p2p/fuzz.go @@ -117,14 +117,15 @@ func (fc *FuzzedConnection) fuzz() bool { case config.FuzzModeDrop: // randomly drop the r/w, drop the conn, or sleep r := cmn.RandFloat64() - if r <= fc.config.ProbDropRW { + switch { + case r <= fc.config.ProbDropRW: return true - } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn { + case r < fc.config.ProbDropRW+fc.config.ProbDropConn: // XXX: can't this fail because machine precision? // XXX: do we need an error? fc.Close() // nolint: errcheck, gas return true - } else if r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep { + case r < fc.config.ProbDropRW+fc.config.ProbDropConn+fc.config.ProbSleep: time.Sleep(fc.randomDuration()) } case config.FuzzModeDelay: diff --git a/p2p/netaddress.go b/p2p/netaddress.go index f39a60543..04a2b843f 100644 --- a/p2p/netaddress.go +++ b/p2p/netaddress.go @@ -250,36 +250,39 @@ func (na *NetAddress) ReachabilityTo(o *NetAddress) int { Ipv4 Ipv6_strong ) - if !na.Routable() { + switch { + case !na.Routable(): return Unreachable - } else if na.RFC4380() { - if !o.Routable() { + case na.RFC4380(): + switch { + case !o.Routable(): return Default - } else if o.RFC4380() { + case o.RFC4380(): return Teredo - } else if o.IP.To4() != nil { + case o.IP.To4() != nil: return Ipv4 - } else { // ipv6 + default: // ipv6 return Ipv6_weak } - } else if na.IP.To4() != nil { + case na.IP.To4() != nil: if o.Routable() && o.IP.To4() != nil { return Ipv4 } return Default - } else /* ipv6 */ { + default: /* ipv6 */ var tunnelled bool // Is our v6 is tunnelled? if o.RFC3964() || o.RFC6052() || o.RFC6145() { tunnelled = true } - if !o.Routable() { + switch { + case !o.Routable(): return Default - } else if o.RFC4380() { + case o.RFC4380(): return Teredo - } else if o.IP.To4() != nil { + case o.IP.To4() != nil: return Ipv4 - } else if tunnelled { + case tunnelled: // only prioritise ipv6 if we aren't tunnelling it. return Ipv6_weak } diff --git a/p2p/node_info_test.go b/p2p/node_info_test.go index 9ed80b28b..e90f397cf 100644 --- a/p2p/node_info_test.go +++ b/p2p/node_info_test.go @@ -31,7 +31,7 @@ func TestNodeInfoValidate(t *testing.T) { malleateNodeInfo func(*DefaultNodeInfo) expectErr bool }{ - {"Too Many Channels", func(ni *DefaultNodeInfo) { ni.Channels = append(channels, byte(maxNumChannels)) }, true}, + {"Too Many Channels", func(ni *DefaultNodeInfo) { ni.Channels = append(channels, byte(maxNumChannels)) }, true}, // nolint: gocritic {"Duplicate Channel", func(ni *DefaultNodeInfo) { ni.Channels = dupChannels }, true}, {"Good Channels", func(ni *DefaultNodeInfo) { ni.Channels = ni.Channels[:5] }, false}, diff --git a/p2p/switch.go b/p2p/switch.go index aad8aa725..6f4a3a193 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -680,8 +680,7 @@ func (sw *Switch) addOutboundPeerWithConfig( metrics: sw.metrics, }) if err != nil { - switch e := err.(type) { - case ErrRejected: + if e, ok := err.(ErrRejected); ok { if e.IsSelf() { // Remove the given address from the address book and add to our addresses // to avoid dialing in the future. diff --git a/rpc/lib/client/http_client.go b/rpc/lib/client/http_client.go index 3b545a5dd..db57c536e 100644 --- a/rpc/lib/client/http_client.go +++ b/rpc/lib/client/http_client.go @@ -42,12 +42,13 @@ func makeHTTPDialer(remoteAddr string) (string, string, func(string, string) (ne parts := strings.SplitN(remoteAddr, "://", 2) var protocol, address string - if len(parts) == 1 { + switch { + case len(parts) == 1: // default to tcp if nothing specified protocol, address = protoTCP, remoteAddr - } else if len(parts) == 2 { + case len(parts) == 2: protocol, address = parts[0], parts[1] - } else { + default: // return a invalid message msg := fmt.Sprintf("Invalid addr: %s", remoteAddr) return clientProtocol, msg, func(_ string, _ string) (net.Conn, error) { diff --git a/rpc/lib/server/handlers.go b/rpc/lib/server/handlers.go index 431f64a41..975b00d57 100644 --- a/rpc/lib/server/handlers.go +++ b/rpc/lib/server/handlers.go @@ -340,13 +340,14 @@ func jsonStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Val func nonJSONStringToArg(cdc *amino.Codec, rt reflect.Type, arg string) (reflect.Value, error, bool) { if rt.Kind() == reflect.Ptr { rv_, err, ok := nonJSONStringToArg(cdc, rt.Elem(), arg) - if err != nil { + switch { + case err != nil: return reflect.Value{}, err, false - } else if ok { + case ok: rv := reflect.New(rt.Elem()) rv.Elem().Set(rv_) return rv, nil, true - } else { + default: return reflect.Value{}, nil, false } } else { diff --git a/state/execution.go b/state/execution.go index 9c4752bb7..5f0fd2fa0 100644 --- a/state/execution.go +++ b/state/execution.go @@ -253,8 +253,7 @@ func execBlockOnProxyApp( // Execute transactions and get hash. proxyCb := func(req *abci.Request, res *abci.Response) { - switch r := res.Value.(type) { - case *abci.Response_DeliverTx: + if r, ok := res.Value.(*abci.Response_DeliverTx); ok { // TODO: make use of res.Log // TODO: make use of this info // Blocks may include invalid txs. diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index e3a5d2f0b..a262ebb26 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -396,7 +396,8 @@ func (txi *TxIndex) match(c query.Condition, startKeyBz []byte, filteredHashes m tmpHashes := make(map[string][]byte) - if c.Op == query.OpEqual { + switch { + case c.Op == query.OpEqual: it := dbm.IteratePrefix(txi.store, startKeyBz) defer it.Close() @@ -404,7 +405,7 @@ func (txi *TxIndex) match(c query.Condition, startKeyBz []byte, filteredHashes m tmpHashes[string(it.Value())] = it.Value() } - } else if c.Op == query.OpContains { + case c.Op == query.OpContains: // 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/" @@ -420,7 +421,7 @@ func (txi *TxIndex) match(c query.Condition, startKeyBz []byte, filteredHashes m tmpHashes[string(it.Value())] = it.Value() } } - } else { + default: panic("other operators should be handled already") } @@ -471,8 +472,7 @@ LOOP: continue } - switch r.AnyBound().(type) { - case int64: + if _, ok := r.AnyBound().(int64); ok { v, err := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) if err != nil { continue LOOP diff --git a/tools/tm-monitor/monitor/network.go b/tools/tm-monitor/monitor/network.go index bb5dd0baa..4d85d7ed6 100644 --- a/tools/tm-monitor/monitor/network.go +++ b/tools/tm-monitor/monitor/network.go @@ -163,11 +163,12 @@ func (n *Network) updateHealth() { // TODO: make sure they're all at the same height (within a block) // and all proposing (and possibly validating ) Alternatively, just // check there hasn't been a new round in numValidators rounds - if n.NumValidators != 0 && n.NumNodesMonitoredOnline == n.NumValidators { + switch { + case n.NumValidators != 0 && n.NumNodesMonitoredOnline == n.NumValidators: n.Health = FullHealth - } else if n.NumNodesMonitoredOnline > 0 && n.NumNodesMonitoredOnline <= n.NumNodesMonitored { + case n.NumNodesMonitoredOnline > 0 && n.NumNodesMonitoredOnline <= n.NumNodesMonitored: n.Health = ModerateHealth - } else { + default: n.Health = Dead } } diff --git a/types/validator.go b/types/validator.go index a662eb6c0..20069ff9a 100644 --- a/types/validator.go +++ b/types/validator.go @@ -41,17 +41,19 @@ func (v *Validator) CompareProposerPriority(other *Validator) *Validator { if v == nil { return other } - if v.ProposerPriority > other.ProposerPriority { + switch { + case v.ProposerPriority > other.ProposerPriority: return v - } else if v.ProposerPriority < other.ProposerPriority { + case v.ProposerPriority < other.ProposerPriority: return other - } else { + default: result := bytes.Compare(v.Address, other.Address) - if result < 0 { + switch { + case result < 0: return v - } else if result > 0 { + case result > 0: return other - } else { + default: panic("Cannot compare identical validators") } } From bb0b63833135043feaed0ce37dca274eb41d3c03 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Fri, 2 Aug 2019 23:42:17 +0400 Subject: [PATCH 166/211] config: move max_msg_bytes into mempool section (#3869) * config: move max_msg_bytes into mempool section It was incorrectly placed in fastsync section during https://github.com/tendermint/tendermint/commit/4d7cd8055b4083376b788239207f72f423e6ce07#diff-092cdc48047eeb4c0bca311a2e1b8ae6 merge. Fixes #3868 * add changelog entry --- CHANGELOG_PENDING.md | 1 + config/toml.go | 6 +++--- docs/tendermint-core/configuration.md | 6 +++--- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 9d659f84f..1097fb4a2 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -14,3 +14,4 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: ### BUG FIXES: +- [config] \#3868 move misplaced `max_msg_bytes` into mempool section diff --git a/config/toml.go b/config/toml.go index fb209bf69..778962538 100644 --- a/config/toml.go +++ b/config/toml.go @@ -371,6 +371,9 @@ max_txs_bytes = {{ .Mempool.MaxTxsBytes }} # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = {{ .Mempool.CacheSize }} +# Limit the size of TxMessage +max_msg_bytes = {{ .Mempool.MaxMsgBytes }} + ##### fast sync configuration options ##### [fastsync] @@ -379,9 +382,6 @@ cache_size = {{ .Mempool.CacheSize }} # 2) "v1" - refactor of v0 version for better testability version = "{{ .FastSync.Version }}" -# Limit the size of TxMessage -max_msg_bytes = {{ .Mempool.MaxMsgBytes }} - ##### consensus configuration options ##### [consensus] diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index d9d96737f..82fcfd61b 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -240,6 +240,9 @@ max_txs_bytes = 1073741824 # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = 10000 +# Limit the size of TxMessage +max_msg_bytes = 1048576 + ##### fast sync configuration options ##### [fastsync] @@ -248,9 +251,6 @@ cache_size = 10000 # 2) "v1" - refactor of v0 version for better testability version = "v0" -# Limit the size of TxMessage -max_msg_bytes = 1048576 - ##### consensus configuration options ##### [consensus] From da5b8acb83abb41d9d451b538586ca925db68705 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Sat, 3 Aug 2019 00:00:18 +0400 Subject: [PATCH 167/211] =?UTF-8?q?consensus:=20reduce=20"Error=20attempti?= =?UTF-8?q?ng=20to=20add=20vote"=20message=20severity=20(Er=E2=80=A6=20(#3?= =?UTF-8?q?871)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * consensus: reduce "Error attempting to add vote" message severity (Error -> Info) Fixes #3839 * add missing changelog entry --- CHANGELOG_PENDING.md | 3 +++ consensus/state.go | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 1097fb4a2..f27ac154a 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -13,5 +13,8 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: +- [consensus] \#3839 Reduce "Error attempting to add vote" message severity (Error -> Info) + ### BUG FIXES: + - [config] \#3868 move misplaced `max_msg_bytes` into mempool section diff --git a/consensus/state.go b/consensus/state.go index 4756fe0e5..af7742f6a 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1518,9 +1518,11 @@ func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, err cs.evpool.AddEvidence(voteErr.DuplicateVoteEvidence) return added, err } else { - // Probably an invalid signature / Bad peer. - // Seems this can also err sometimes with "Unexpected step" - perhaps not from a bad peer ? - cs.Logger.Error("Error attempting to add vote", "err", err) + // Either + // 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). + cs.Logger.Info("Error attempting to add vote", "err", err) return added, ErrAddingVote } } From 84e2e19a1577417e35f77f587392ef77cef1eddc Mon Sep 17 00:00:00 2001 From: Juan Leni Date: Mon, 5 Aug 2019 17:09:10 +0200 Subject: [PATCH 168/211] privval: refactor Remote signers (#3370) This PR is related to #3107 and a continuation of #3351 It is important to emphasise that in the privval original design, client/server and listening/dialing roles are inverted and do not follow a conventional interaction. Given two hosts A and B: Host A is listener/client Host B is dialer/server (contains the secret key) When A requires a signature, it needs to wait for B to dial in before it can issue a request. A only accepts a single connection and any failure leads to dropping the connection and waiting for B to reconnect. The original rationale behind this design was based on security. Host B only allows outbound connections to a list of whitelisted hosts. It is not possible to reach B unless B dials in. There are no listening/open ports in B. This PR results in the following changes: Refactors ping/heartbeat to avoid previously existing race conditions. Separates transport (dialer/listener) from signing (client/server) concerns to simplify workflow. Unifies and abstracts away the differences between unix and tcp sockets. A single signer endpoint implementation unifies connection handling code (read/write/close/connection obj) The signer request handler (server side) is customizable to increase testability. Updates and extends unit tests A high level overview of the classes is as follows: Transport (endpoints): The following classes take care of establishing a connection SignerDialerEndpoint SignerListeningEndpoint SignerEndpoint groups common functionality (read/write/timeouts/etc.) Signing (client/server): The following classes take care of exchanging request/responses SignerClient SignerServer This PR also closes #3601 Commits: * refactoring - work in progress * reworking unit tests * Encapsulating and fixing unit tests * Improve tests * Clean up * Fix/improve unit tests * clean up tests * Improving service endpoint * fixing unit test * fix linter issues * avoid invalid cache values (improve later?) * complete implementation * wip * improved connection loop * Improve reconnections + fixing unit tests * addressing comments * small formatting changes * clean up * Update node/node.go Co-Authored-By: jleni * Update privval/signer_client.go Co-Authored-By: jleni * Update privval/signer_client_test.go Co-Authored-By: jleni * check during initialization * dropping connecting when writing fails * removing break * use t.log instead * unifying and using cmn.GetFreePort() * review fixes * reordering and unifying drop connection * closing instead of signalling * refactored service loop * removed superfluous brackets * GetPubKey can return errors * Revert "GetPubKey can return errors" This reverts commit 68c06f19b4650389d7e5ab1659b318889028202c. * adding entry to changelog * Update CHANGELOG_PENDING.md Co-Authored-By: jleni * Update privval/signer_client.go Co-Authored-By: jleni * Update privval/signer_dialer_endpoint.go Co-Authored-By: jleni * Update privval/signer_dialer_endpoint.go Co-Authored-By: jleni * Update privval/signer_dialer_endpoint.go Co-Authored-By: jleni * Update privval/signer_dialer_endpoint.go Co-Authored-By: jleni * Update privval/signer_listener_endpoint_test.go Co-Authored-By: jleni * updating node.go * review fixes * fixes linter * fixing unit test * small fixes in comments * addressing review comments * addressing review comments 2 * reverting suggestion * Update privval/signer_client_test.go Co-Authored-By: Anton Kaliaev * Update privval/signer_client_test.go Co-Authored-By: Anton Kaliaev * Update privval/signer_listener_endpoint_test.go Co-Authored-By: Anton Kaliaev * do not expose brokenSignerDialerEndpoint * clean up logging * unifying methods shorten test time signer also drops * reenabling pings * improving testability + unit test * fixing go fmt + unit test * remove unused code * Addressing review comments * simplifying connection workflow * fix linter/go import issue * using base service quit * updating comment * Simplifying design + adjusting names * fixing linter issues * refactoring test harness + fixes * Addressing review comments * cleaning up * adding additional error check --- .gitignore | 2 + CHANGELOG_PENDING.md | 1 + cmd/priv_val_server/main.go | 8 +- node/node.go | 32 +- node/node_test.go | 29 +- privval/doc.go | 12 +- privval/errors.go | 15 +- privval/file_deprecated_test.go | 8 +- privval/file_test.go | 3 +- privval/messages.go | 25 +- privval/signer_client.go | 131 +++++ privval/signer_client_test.go | 257 +++++++++ privval/signer_dialer_endpoint.go | 84 +++ privval/signer_endpoint.go | 156 ++++++ privval/signer_listener_endpoint.go | 198 +++++++ privval/signer_listener_endpoint_test.go | 198 +++++++ privval/signer_remote.go | 203 ------- privval/signer_remote_test.go | 68 --- privval/signer_requestHandler.go | 44 ++ privval/signer_server.go | 107 ++++ privval/signer_service_endpoint.go | 139 ----- privval/signer_validator_endpoint.go | 237 -------- privval/signer_validator_endpoint_test.go | 506 ------------------ privval/socket_dialers_test.go | 29 +- privval/socket_listeners.go | 4 +- privval/utils.go | 43 +- privval/utils_test.go | 1 + .../internal/test_harness.go | 44 +- .../internal/test_harness_test.go | 56 +- types/priv_validator.go | 4 +- 30 files changed, 1369 insertions(+), 1275 deletions(-) create mode 100644 privval/signer_client.go create mode 100644 privval/signer_client_test.go create mode 100644 privval/signer_dialer_endpoint.go create mode 100644 privval/signer_endpoint.go create mode 100644 privval/signer_listener_endpoint.go create mode 100644 privval/signer_listener_endpoint_test.go delete mode 100644 privval/signer_remote.go delete mode 100644 privval/signer_remote_test.go create mode 100644 privval/signer_requestHandler.go create mode 100644 privval/signer_server.go delete mode 100644 privval/signer_service_endpoint.go delete mode 100644 privval/signer_validator_endpoint.go delete mode 100644 privval/signer_validator_endpoint_test.go diff --git a/.gitignore b/.gitignore index 10ee3099c..9e2e5a9ea 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,5 @@ terraform.tfstate.backup terraform.tfstate.d .vscode + +profile\.out diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index f27ac154a..4b53a3941 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -13,6 +13,7 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: +- [privval] \#3370 Refactors and simplifies validator/kms connection handling. Please refer to thttps://github.com/tendermint/tendermint/pull/3370#issue-257360971 - [consensus] \#3839 Reduce "Error attempting to add vote" message severity (Error -> Info) ### BUG FIXES: diff --git a/cmd/priv_val_server/main.go b/cmd/priv_val_server/main.go index c86bced81..22af6418f 100644 --- a/cmd/priv_val_server/main.go +++ b/cmd/priv_val_server/main.go @@ -48,15 +48,17 @@ func main() { os.Exit(1) } - rs := privval.NewSignerServiceEndpoint(logger, *chainID, pv, dialer) - err := rs.Start() + sd := privval.NewSignerDialerEndpoint(logger, dialer) + ss := privval.NewSignerServer(sd, *chainID, pv) + + err := ss.Start() if err != nil { panic(err) } // Stop upon receiving SIGTERM or CTRL-C. cmn.TrapSignal(logger, func() { - err := rs.Stop() + err := ss.Stop() if err != nil { panic(err) } diff --git a/node/node.go b/node/node.go index 548914ffe..bef83b290 100644 --- a/node/node.go +++ b/node/node.go @@ -24,7 +24,7 @@ import ( cfg "github.com/tendermint/tendermint/config" "github.com/tendermint/tendermint/consensus" cs "github.com/tendermint/tendermint/consensus" - "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/evidence" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" @@ -316,9 +316,8 @@ func doHandshake(stateDB dbm.DB, state sm.State, blockStore sm.BlockStore, return nil } -func logNodeStartupInfo(state sm.State, privValidator types.PrivValidator, config *cfg.Config, logger, +func logNodeStartupInfo(state sm.State, pubKey crypto.PubKey, config *cfg.Config, logger, consensusLogger log.Logger) { - // Log the version info. logger.Info("Version info", "software", version.TMCoreSemVer, @@ -334,7 +333,6 @@ func logNodeStartupInfo(state sm.State, privValidator types.PrivValidator, confi ) } - pubKey := privValidator.GetPubKey() addr := pubKey.Address() // Log whether this node is a validator or an observer if state.Validators.HasAddress(addr) { @@ -668,7 +666,7 @@ func NewNode(config *cfg.Config, return nil, errors.New("could not retrieve public key from private validator") } - logNodeStartupInfo(state, privValidator, config, logger, consensusLogger) + logNodeStartupInfo(state, pubKey, config, logger, consensusLogger) // Decide whether to fast-sync or not // We don't fast-sync when the only validator is us. @@ -1253,29 +1251,13 @@ func createAndStartPrivValidatorSocketClient( listenAddr string, logger log.Logger, ) (types.PrivValidator, error) { - var listener net.Listener - - protocol, address := cmn.ProtocolAndAddress(listenAddr) - ln, err := net.Listen(protocol, address) + pve, err := privval.NewSignerListener(listenAddr, logger) if err != nil { - return nil, err - } - switch protocol { - case "unix": - listener = privval.NewUnixListener(ln) - case "tcp": - // TODO: persist this key so external signer - // can actually authenticate us - listener = privval.NewTCPListener(ln, ed25519.GenPrivKey()) - default: - return nil, fmt.Errorf( - "wrong listen address: expected either 'tcp' or 'unix' protocols, got %s", - protocol, - ) + return nil, errors.Wrap(err, "failed to start private validator") } - pvsc := privval.NewSignerValidatorEndpoint(logger.With("module", "privval"), listener) - if err := pvsc.Start(); err != nil { + pvsc, err := privval.NewSignerClient(pve) + if err != nil { return nil, errors.Wrap(err, "failed to start private validator") } diff --git a/node/node_test.go b/node/node_test.go index 1d646ea74..77e970d28 100644 --- a/node/node_test.go +++ b/node/node_test.go @@ -136,25 +136,29 @@ func TestNodeSetPrivValTCP(t *testing.T) { config.BaseConfig.PrivValidatorListenAddr = addr dialer := privval.DialTCPFn(addr, 100*time.Millisecond, ed25519.GenPrivKey()) - pvsc := privval.NewSignerServiceEndpoint( + dialerEndpoint := privval.NewSignerDialerEndpoint( log.TestingLogger(), + dialer, + ) + privval.SignerDialerEndpointTimeoutReadWrite(100 * time.Millisecond)(dialerEndpoint) + + signerServer := privval.NewSignerServer( + dialerEndpoint, config.ChainID(), types.NewMockPV(), - dialer, ) - privval.SignerServiceEndpointTimeoutReadWrite(100 * time.Millisecond)(pvsc) go func() { - err := pvsc.Start() + err := signerServer.Start() if err != nil { panic(err) } }() - defer pvsc.Stop() + defer signerServer.Stop() n, err := DefaultNewNode(config, log.TestingLogger()) require.NoError(t, err) - assert.IsType(t, &privval.SignerValidatorEndpoint{}, n.PrivValidator()) + assert.IsType(t, &privval.SignerClient{}, n.PrivValidator()) } // address without a protocol must result in error @@ -178,13 +182,17 @@ func TestNodeSetPrivValIPC(t *testing.T) { config.BaseConfig.PrivValidatorListenAddr = "unix://" + tmpfile dialer := privval.DialUnixFn(tmpfile) - pvsc := privval.NewSignerServiceEndpoint( + dialerEndpoint := privval.NewSignerDialerEndpoint( log.TestingLogger(), + dialer, + ) + privval.SignerDialerEndpointTimeoutReadWrite(100 * time.Millisecond)(dialerEndpoint) + + pvsc := privval.NewSignerServer( + dialerEndpoint, config.ChainID(), types.NewMockPV(), - dialer, ) - privval.SignerServiceEndpointTimeoutReadWrite(100 * time.Millisecond)(pvsc) go func() { err := pvsc.Start() @@ -194,8 +202,7 @@ func TestNodeSetPrivValIPC(t *testing.T) { n, err := DefaultNewNode(config, log.TestingLogger()) require.NoError(t, err) - assert.IsType(t, &privval.SignerValidatorEndpoint{}, n.PrivValidator()) - + assert.IsType(t, &privval.SignerClient{}, n.PrivValidator()) } // testFreeAddr claims a free port so we don't block on listener being ready. diff --git a/privval/doc.go b/privval/doc.go index 80869a6a7..ad60673b6 100644 --- a/privval/doc.go +++ b/privval/doc.go @@ -6,16 +6,16 @@ FilePV FilePV is the simplest implementation and developer default. It uses one file for the private key and another to store state. -SignerValidatorEndpoint +SignerListenerEndpoint -SignerValidatorEndpoint establishes a connection to an external process, like a Key Management Server (KMS), using a socket. -SignerValidatorEndpoint listens for the external KMS process to dial in. -SignerValidatorEndpoint takes a listener, which determines the type of connection +SignerListenerEndpoint establishes a connection to an external process, like a Key Management Server (KMS), using a socket. +SignerListenerEndpoint listens for the external KMS process to dial in. +SignerListenerEndpoint takes a listener, which determines the type of connection (ie. encrypted over tcp, or unencrypted over unix). -SignerServiceEndpoint +SignerDialerEndpoint -SignerServiceEndpoint is a simple wrapper around a net.Conn. It's used by both IPCVal and TCPVal. +SignerDialerEndpoint is a simple wrapper around a net.Conn. It's used by both IPCVal and TCPVal. */ package privval diff --git a/privval/errors.go b/privval/errors.go index 75fb25fc6..9f151f11d 100644 --- a/privval/errors.go +++ b/privval/errors.go @@ -4,10 +4,21 @@ import ( "fmt" ) +type EndpointTimeoutError struct{} + +// Implement the net.Error interface. +func (e EndpointTimeoutError) Error() string { return "endpoint connection timed out" } +func (e EndpointTimeoutError) Timeout() bool { return true } +func (e EndpointTimeoutError) Temporary() bool { return true } + // Socket errors. var ( ErrUnexpectedResponse = fmt.Errorf("received unexpected response") - ErrConnTimeout = fmt.Errorf("remote signer timed out") + ErrNoConnection = fmt.Errorf("endpoint is not connected") + ErrConnectionTimeout = EndpointTimeoutError{} + + ErrReadTimeout = fmt.Errorf("endpoint read timed out") + ErrWriteTimeout = fmt.Errorf("endpoint write timed out") ) // RemoteSignerError allows (remote) validators to include meaningful error descriptions in their reply. @@ -18,5 +29,5 @@ type RemoteSignerError struct { } func (e *RemoteSignerError) Error() string { - return fmt.Sprintf("signerServiceEndpoint returned error #%d: %s", e.Code, e.Description) + return fmt.Sprintf("signerEndpoint returned error #%d: %s", e.Code, e.Description) } diff --git a/privval/file_deprecated_test.go b/privval/file_deprecated_test.go index 46391a3fe..e678bfc09 100644 --- a/privval/file_deprecated_test.go +++ b/privval/file_deprecated_test.go @@ -67,11 +67,11 @@ func assertEqualPV(t *testing.T, oldPV *privval.OldFilePV, newPV *privval.FilePV } func initTmpOldFile(t *testing.T) string { - tmpfile, err := ioutil.TempFile("", "priv_validator_*.json") + tmpFile, err := ioutil.TempFile("", "priv_validator_*.json") require.NoError(t, err) - t.Logf("created test file %s", tmpfile.Name()) - _, err = tmpfile.WriteString(oldPrivvalContent) + t.Logf("created test file %s", tmpFile.Name()) + _, err = tmpFile.WriteString(oldPrivvalContent) require.NoError(t, err) - return tmpfile.Name() + return tmpFile.Name() } diff --git a/privval/file_test.go b/privval/file_test.go index 98de69480..38f6e6fe3 100644 --- a/privval/file_test.go +++ b/privval/file_test.go @@ -58,7 +58,7 @@ func TestResetValidator(t *testing.T) { // priv val after signing is not same as empty assert.NotEqual(t, privVal.LastSignState, emptyState) - // priv val after reset is same as empty + // priv val after AcceptNewConnection is same as empty privVal.Reset() assert.Equal(t, privVal.LastSignState, emptyState) } @@ -164,6 +164,7 @@ func TestSignVote(t *testing.T) { block1 := types.BlockID{Hash: []byte{1, 2, 3}, PartsHeader: types.PartSetHeader{}} block2 := types.BlockID{Hash: []byte{3, 2, 1}, PartsHeader: types.PartSetHeader{}} + height, round := int64(10), 1 voteType := byte(types.PrevoteType) diff --git a/privval/messages.go b/privval/messages.go index 6774a2795..7704049fe 100644 --- a/privval/messages.go +++ b/privval/messages.go @@ -1,61 +1,64 @@ package privval import ( - amino "github.com/tendermint/go-amino" + "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/types" ) -// RemoteSignerMsg is sent between SignerServiceEndpoint and the SignerServiceEndpoint client. -type RemoteSignerMsg interface{} +// SignerMessage is sent between Signer Clients and Servers. +type SignerMessage interface{} func RegisterRemoteSignerMsg(cdc *amino.Codec) { - cdc.RegisterInterface((*RemoteSignerMsg)(nil), nil) + cdc.RegisterInterface((*SignerMessage)(nil), nil) cdc.RegisterConcrete(&PubKeyRequest{}, "tendermint/remotesigner/PubKeyRequest", nil) cdc.RegisterConcrete(&PubKeyResponse{}, "tendermint/remotesigner/PubKeyResponse", nil) cdc.RegisterConcrete(&SignVoteRequest{}, "tendermint/remotesigner/SignVoteRequest", nil) cdc.RegisterConcrete(&SignedVoteResponse{}, "tendermint/remotesigner/SignedVoteResponse", nil) cdc.RegisterConcrete(&SignProposalRequest{}, "tendermint/remotesigner/SignProposalRequest", nil) cdc.RegisterConcrete(&SignedProposalResponse{}, "tendermint/remotesigner/SignedProposalResponse", nil) + cdc.RegisterConcrete(&PingRequest{}, "tendermint/remotesigner/PingRequest", nil) cdc.RegisterConcrete(&PingResponse{}, "tendermint/remotesigner/PingResponse", nil) } +// TODO: Add ChainIDRequest + // PubKeyRequest requests the consensus public key from the remote signer. type PubKeyRequest struct{} -// PubKeyResponse is a PrivValidatorSocket message containing the public key. +// PubKeyResponse is a response message containing the public key. type PubKeyResponse struct { PubKey crypto.PubKey Error *RemoteSignerError } -// SignVoteRequest is a PrivValidatorSocket message containing a vote. +// SignVoteRequest is a request to sign a vote type SignVoteRequest struct { Vote *types.Vote } -// SignedVoteResponse is a PrivValidatorSocket message containing a signed vote along with a potenial error message. +// SignedVoteResponse is a response containing a signed vote or an error type SignedVoteResponse struct { Vote *types.Vote Error *RemoteSignerError } -// SignProposalRequest is a PrivValidatorSocket message containing a Proposal. +// SignProposalRequest is a request to sign a proposal type SignProposalRequest struct { Proposal *types.Proposal } -// SignedProposalResponse is a PrivValidatorSocket message containing a proposal response +// SignedProposalResponse is response containing a signed proposal or an error type SignedProposalResponse struct { Proposal *types.Proposal Error *RemoteSignerError } -// PingRequest is a PrivValidatorSocket message to keep the connection alive. +// PingRequest is a request to confirm that the connection is alive. type PingRequest struct { } -// PingRequest is a PrivValidatorSocket response to keep the connection alive. +// PingResponse is a response to confirm that the connection is alive. type PingResponse struct { } diff --git a/privval/signer_client.go b/privval/signer_client.go new file mode 100644 index 000000000..0885ee4aa --- /dev/null +++ b/privval/signer_client.go @@ -0,0 +1,131 @@ +package privval + +import ( + "time" + + "github.com/pkg/errors" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/types" +) + +// SignerClient implements PrivValidator. +// Handles remote validator connections that provide signing services +type SignerClient struct { + endpoint *SignerListenerEndpoint +} + +var _ types.PrivValidator = (*SignerClient)(nil) + +// NewSignerClient returns an instance of SignerClient. +// it will start the endpoint (if not already started) +func NewSignerClient(endpoint *SignerListenerEndpoint) (*SignerClient, error) { + if !endpoint.IsRunning() { + if err := endpoint.Start(); err != nil { + return nil, errors.Wrap(err, "failed to start listener endpoint") + } + } + + return &SignerClient{endpoint: endpoint}, nil +} + +// Close closes the underlying connection +func (sc *SignerClient) Close() error { + return sc.endpoint.Close() +} + +// IsConnected indicates with the signer is connected to a remote signing service +func (sc *SignerClient) IsConnected() bool { + return sc.endpoint.IsConnected() +} + +// WaitForConnection waits maxWait for a connection or returns a timeout error +func (sc *SignerClient) WaitForConnection(maxWait time.Duration) error { + return sc.endpoint.WaitForConnection(maxWait) +} + +//-------------------------------------------------------- +// Implement PrivValidator + +// Ping sends a ping request to the remote signer +func (sc *SignerClient) Ping() error { + response, err := sc.endpoint.SendRequest(&PingRequest{}) + + if err != nil { + sc.endpoint.Logger.Error("SignerClient::Ping", "err", err) + return nil + } + + _, ok := response.(*PingResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::Ping", "err", "response != PingResponse") + return err + } + + return nil +} + +// GetPubKey retrieves a public key from a remote signer +func (sc *SignerClient) GetPubKey() crypto.PubKey { + response, err := sc.endpoint.SendRequest(&PubKeyRequest{}) + if err != nil { + sc.endpoint.Logger.Error("SignerClient::GetPubKey", "err", err) + return nil + } + + pubKeyResp, ok := response.(*PubKeyResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::GetPubKey", "err", "response != PubKeyResponse") + return nil + } + + if pubKeyResp.Error != nil { + sc.endpoint.Logger.Error("failed to get private validator's public key", "err", pubKeyResp.Error) + return nil + } + + return pubKeyResp.PubKey +} + +// SignVote requests a remote signer to sign a vote +func (sc *SignerClient) SignVote(chainID string, vote *types.Vote) error { + response, err := sc.endpoint.SendRequest(&SignVoteRequest{Vote: vote}) + if err != nil { + sc.endpoint.Logger.Error("SignerClient::SignVote", "err", err) + return err + } + + resp, ok := response.(*SignedVoteResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::GetPubKey", "err", "response != SignedVoteResponse") + return ErrUnexpectedResponse + } + + if resp.Error != nil { + return resp.Error + } + *vote = *resp.Vote + + return nil +} + +// SignProposal requests a remote signer to sign a proposal +func (sc *SignerClient) SignProposal(chainID string, proposal *types.Proposal) error { + response, err := sc.endpoint.SendRequest(&SignProposalRequest{Proposal: proposal}) + if err != nil { + sc.endpoint.Logger.Error("SignerClient::SignProposal", "err", err) + return err + } + + resp, ok := response.(*SignedProposalResponse) + if !ok { + sc.endpoint.Logger.Error("SignerClient::SignProposal", "err", "response != SignedProposalResponse") + return ErrUnexpectedResponse + } + if resp.Error != nil { + return resp.Error + } + *proposal = *resp.Proposal + + return nil +} diff --git a/privval/signer_client_test.go b/privval/signer_client_test.go new file mode 100644 index 000000000..3d7cfb3e0 --- /dev/null +++ b/privval/signer_client_test.go @@ -0,0 +1,257 @@ +package privval + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/types" +) + +type signerTestCase struct { + chainID string + mockPV types.PrivValidator + signerClient *SignerClient + signerServer *SignerServer +} + +func getSignerTestCases(t *testing.T) []signerTestCase { + testCases := make([]signerTestCase, 0) + + // Get test cases for each possible dialer (DialTCP / DialUnix / etc) + for _, dtc := range getDialerTestCases(t) { + chainID := common.RandStr(12) + mockPV := types.NewMockPV() + + // get a pair of signer listener, signer dialer endpoints + sl, sd := getMockEndpoints(t, dtc.addr, dtc.dialer) + sc, err := NewSignerClient(sl) + require.NoError(t, err) + ss := NewSignerServer(sd, chainID, mockPV) + + err = ss.Start() + require.NoError(t, err) + + tc := signerTestCase{ + chainID: chainID, + mockPV: mockPV, + signerClient: sc, + signerServer: ss, + } + + testCases = append(testCases, tc) + } + + return testCases +} + +func TestSignerClose(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + err := tc.signerClient.Close() + assert.NoError(t, err) + + err = tc.signerServer.Stop() + assert.NoError(t, err) + } +} + +func TestSignerPing(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + err := tc.signerClient.Ping() + assert.NoError(t, err) + } +} + +func TestSignerGetPubKey(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + pubKey := tc.signerClient.GetPubKey() + expectedPubKey := tc.mockPV.GetPubKey() + + assert.Equal(t, expectedPubKey, pubKey) + + addr := tc.signerClient.GetPubKey().Address() + expectedAddr := tc.mockPV.GetPubKey().Address() + + assert.Equal(t, expectedAddr, addr) + } +} + +func TestSignerProposal(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Proposal{Timestamp: ts} + have := &types.Proposal{Timestamp: ts} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + require.NoError(t, tc.mockPV.SignProposal(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignProposal(tc.chainID, have)) + + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerVote(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + have := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerVoteResetDeadline(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + have := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + time.Sleep(testTimeoutReadWrite2o3) + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + assert.Equal(t, want.Signature, have.Signature) + + // TODO(jleni): Clarify what is actually being tested + + // This would exceed the deadline if it was not extended by the previous message + time.Sleep(testTimeoutReadWrite2o3) + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerVoteKeepAlive(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + have := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + // Check that even if the client does not request a + // signature for a long time. The service is still available + + // in this particular case, we use the dialer logger to ensure that + // test messages are properly interleaved in the test logs + tc.signerServer.Logger.Debug("TEST: Forced Wait -------------------------------------------------") + time.Sleep(testTimeoutReadWrite * 3) + tc.signerServer.Logger.Debug("TEST: Forced Wait DONE---------------------------------------------") + + require.NoError(t, tc.mockPV.SignVote(tc.chainID, want)) + require.NoError(t, tc.signerClient.SignVote(tc.chainID, have)) + + assert.Equal(t, want.Signature, have.Signature) + } +} + +func TestSignerSignProposalErrors(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + // Replace service with a mock that always fails + tc.signerServer.privVal = types.NewErroringMockPV() + tc.mockPV = types.NewErroringMockPV() + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + ts := time.Now() + proposal := &types.Proposal{Timestamp: ts} + err := tc.signerClient.SignProposal(tc.chainID, proposal) + require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) + + err = tc.mockPV.SignProposal(tc.chainID, proposal) + require.Error(t, err) + + err = tc.signerClient.SignProposal(tc.chainID, proposal) + require.Error(t, err) + } +} + +func TestSignerSignVoteErrors(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + ts := time.Now() + vote := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + // Replace signer service privval with one that always fails + tc.signerServer.privVal = types.NewErroringMockPV() + tc.mockPV = types.NewErroringMockPV() + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + err := tc.signerClient.SignVote(tc.chainID, vote) + require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) + + err = tc.mockPV.SignVote(tc.chainID, vote) + require.Error(t, err) + + err = tc.signerClient.SignVote(tc.chainID, vote) + require.Error(t, err) + } +} + +func brokenHandler(privVal types.PrivValidator, request SignerMessage, chainID string) (SignerMessage, error) { + var res SignerMessage + var err error + + switch r := request.(type) { + + // This is broken and will answer most requests with a pubkey response + case *PubKeyRequest: + res = &PubKeyResponse{nil, nil} + case *SignVoteRequest: + res = &PubKeyResponse{nil, nil} + case *SignProposalRequest: + res = &PubKeyResponse{nil, nil} + + case *PingRequest: + err, res = nil, &PingResponse{} + + default: + err = fmt.Errorf("unknown msg: %v", r) + } + + return res, err +} + +func TestSignerUnexpectedResponse(t *testing.T) { + for _, tc := range getSignerTestCases(t) { + tc.signerServer.privVal = types.NewMockPV() + tc.mockPV = types.NewMockPV() + + tc.signerServer.SetRequestHandler(brokenHandler) + + defer tc.signerServer.Stop() + defer tc.signerClient.Close() + + ts := time.Now() + want := &types.Vote{Timestamp: ts, Type: types.PrecommitType} + + e := tc.signerClient.SignVote(tc.chainID, want) + assert.EqualError(t, e, "received unexpected response") + } +} diff --git a/privval/signer_dialer_endpoint.go b/privval/signer_dialer_endpoint.go new file mode 100644 index 000000000..95094c6d0 --- /dev/null +++ b/privval/signer_dialer_endpoint.go @@ -0,0 +1,84 @@ +package privval + +import ( + "time" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" +) + +const ( + defaultMaxDialRetries = 10 + defaultRetryWaitMilliseconds = 100 +) + +// SignerServiceEndpointOption sets an optional parameter on the SignerDialerEndpoint. +type SignerServiceEndpointOption func(*SignerDialerEndpoint) + +// SignerDialerEndpointTimeoutReadWrite sets the read and write timeout for connections +// from external signing processes. +func SignerDialerEndpointTimeoutReadWrite(timeout time.Duration) SignerServiceEndpointOption { + return func(ss *SignerDialerEndpoint) { ss.timeoutReadWrite = timeout } +} + +// SignerDialerEndpointConnRetries sets the amount of attempted retries to acceptNewConnection. +func SignerDialerEndpointConnRetries(retries int) SignerServiceEndpointOption { + return func(ss *SignerDialerEndpoint) { ss.maxConnRetries = retries } +} + +// SignerDialerEndpoint dials using its dialer and responds to any +// signature requests using its privVal. +type SignerDialerEndpoint struct { + signerEndpoint + + dialer SocketDialer + + retryWait time.Duration + maxConnRetries int +} + +// NewSignerDialerEndpoint returns a SignerDialerEndpoint that will dial using the given +// dialer and respond to any signature requests over the connection +// using the given privVal. +func NewSignerDialerEndpoint( + logger log.Logger, + dialer SocketDialer, +) *SignerDialerEndpoint { + + sd := &SignerDialerEndpoint{ + dialer: dialer, + retryWait: defaultRetryWaitMilliseconds * time.Millisecond, + maxConnRetries: defaultMaxDialRetries, + } + + sd.BaseService = *cmn.NewBaseService(logger, "SignerDialerEndpoint", sd) + sd.signerEndpoint.timeoutReadWrite = defaultTimeoutReadWriteSeconds * time.Second + + return sd +} + +func (sd *SignerDialerEndpoint) ensureConnection() error { + if sd.IsConnected() { + return nil + } + + retries := 0 + for retries < sd.maxConnRetries { + conn, err := sd.dialer() + + if err != nil { + retries++ + sd.Logger.Debug("SignerDialer: Reconnection failed", "retries", retries, "max", sd.maxConnRetries, "err", err) + // Wait between retries + time.Sleep(sd.retryWait) + } else { + sd.SetConnection(conn) + sd.Logger.Debug("SignerDialer: Connection Ready") + return nil + } + } + + sd.Logger.Debug("SignerDialer: Max retries exceeded", "retries", retries, "max", sd.maxConnRetries) + + return ErrNoConnection +} diff --git a/privval/signer_endpoint.go b/privval/signer_endpoint.go new file mode 100644 index 000000000..425f73fea --- /dev/null +++ b/privval/signer_endpoint.go @@ -0,0 +1,156 @@ +package privval + +import ( + "fmt" + "net" + "sync" + "time" + + "github.com/pkg/errors" + + cmn "github.com/tendermint/tendermint/libs/common" +) + +const ( + defaultTimeoutReadWriteSeconds = 3 +) + +type signerEndpoint struct { + cmn.BaseService + + connMtx sync.Mutex + conn net.Conn + + timeoutReadWrite time.Duration +} + +// Close closes the underlying net.Conn. +func (se *signerEndpoint) Close() error { + se.DropConnection() + return nil +} + +// IsConnected indicates if there is an active connection +func (se *signerEndpoint) IsConnected() bool { + se.connMtx.Lock() + defer se.connMtx.Unlock() + return se.isConnected() +} + +// TryGetConnection retrieves a connection if it is already available +func (se *signerEndpoint) GetAvailableConnection(connectionAvailableCh chan net.Conn) bool { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + // Is there a connection ready? + select { + case se.conn = <-connectionAvailableCh: + return true + default: + } + return false +} + +// TryGetConnection retrieves a connection if it is already available +func (se *signerEndpoint) WaitConnection(connectionAvailableCh chan net.Conn, maxWait time.Duration) error { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + select { + case se.conn = <-connectionAvailableCh: + case <-time.After(maxWait): + return ErrConnectionTimeout + } + + return nil +} + +// SetConnection replaces the current connection object +func (se *signerEndpoint) SetConnection(newConnection net.Conn) { + se.connMtx.Lock() + defer se.connMtx.Unlock() + se.conn = newConnection +} + +// IsConnected indicates if there is an active connection +func (se *signerEndpoint) DropConnection() { + se.connMtx.Lock() + defer se.connMtx.Unlock() + se.dropConnection() +} + +// ReadMessage reads a message from the endpoint +func (se *signerEndpoint) ReadMessage() (msg SignerMessage, err error) { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + if !se.isConnected() { + return nil, fmt.Errorf("endpoint is not connected") + } + + // Reset read deadline + deadline := time.Now().Add(se.timeoutReadWrite) + + err = se.conn.SetReadDeadline(deadline) + if err != nil { + return + } + + const maxRemoteSignerMsgSize = 1024 * 10 + _, err = cdc.UnmarshalBinaryLengthPrefixedReader(se.conn, &msg, maxRemoteSignerMsgSize) + if _, ok := err.(timeoutError); ok { + if err != nil { + err = errors.Wrap(ErrReadTimeout, err.Error()) + } else { + err = errors.Wrap(ErrReadTimeout, "Empty error") + } + se.Logger.Debug("Dropping [read]", "obj", se) + se.dropConnection() + } + + return +} + +// WriteMessage writes a message from the endpoint +func (se *signerEndpoint) WriteMessage(msg SignerMessage) (err error) { + se.connMtx.Lock() + defer se.connMtx.Unlock() + + if !se.isConnected() { + return errors.Wrap(ErrNoConnection, "endpoint is not connected") + } + + // Reset read deadline + deadline := time.Now().Add(se.timeoutReadWrite) + se.Logger.Debug("Write::Error Resetting deadline", "obj", se) + + err = se.conn.SetWriteDeadline(deadline) + if err != nil { + return + } + + _, err = cdc.MarshalBinaryLengthPrefixedWriter(se.conn, msg) + if _, ok := err.(timeoutError); ok { + if err != nil { + err = errors.Wrap(ErrWriteTimeout, err.Error()) + } else { + err = errors.Wrap(ErrWriteTimeout, "Empty error") + } + se.dropConnection() + } + + return +} + +func (se *signerEndpoint) isConnected() bool { + return se.conn != nil +} + +func (se *signerEndpoint) dropConnection() { + if se.conn != nil { + if err := se.conn.Close(); err != nil { + se.Logger.Error("signerEndpoint::dropConnection", "err", err) + } + se.conn = nil + } +} diff --git a/privval/signer_listener_endpoint.go b/privval/signer_listener_endpoint.go new file mode 100644 index 000000000..e25f18756 --- /dev/null +++ b/privval/signer_listener_endpoint.go @@ -0,0 +1,198 @@ +package privval + +import ( + "fmt" + "net" + "sync" + "time" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" +) + +// SignerValidatorEndpointOption sets an optional parameter on the SocketVal. +type SignerValidatorEndpointOption func(*SignerListenerEndpoint) + +// SignerListenerEndpoint listens for an external process to dial in +// and keeps the connection alive by dropping and reconnecting +type SignerListenerEndpoint struct { + signerEndpoint + + listener net.Listener + connectRequestCh chan struct{} + connectionAvailableCh chan net.Conn + + timeoutAccept time.Duration + pingTimer *time.Ticker + + instanceMtx sync.Mutex // Ensures instance public methods access, i.e. SendRequest +} + +// NewSignerListenerEndpoint returns an instance of SignerListenerEndpoint. +func NewSignerListenerEndpoint( + logger log.Logger, + listener net.Listener, +) *SignerListenerEndpoint { + sc := &SignerListenerEndpoint{ + listener: listener, + timeoutAccept: defaultTimeoutAcceptSeconds * time.Second, + } + + sc.BaseService = *cmn.NewBaseService(logger, "SignerListenerEndpoint", sc) + sc.signerEndpoint.timeoutReadWrite = defaultTimeoutReadWriteSeconds * time.Second + return sc +} + +// OnStart implements cmn.Service. +func (sl *SignerListenerEndpoint) OnStart() error { + sl.connectRequestCh = make(chan struct{}) + sl.connectionAvailableCh = make(chan net.Conn) + + sl.pingTimer = time.NewTicker(defaultPingPeriodMilliseconds * time.Millisecond) + + go sl.serviceLoop() + go sl.pingLoop() + + sl.connectRequestCh <- struct{}{} + + return nil +} + +// OnStop implements cmn.Service +func (sl *SignerListenerEndpoint) OnStop() { + sl.instanceMtx.Lock() + defer sl.instanceMtx.Unlock() + _ = sl.Close() + + // Stop listening + if sl.listener != nil { + if err := sl.listener.Close(); err != nil { + sl.Logger.Error("Closing Listener", "err", err) + sl.listener = nil + } + } + + sl.pingTimer.Stop() +} + +// WaitForConnection waits maxWait for a connection or returns a timeout error +func (sl *SignerListenerEndpoint) WaitForConnection(maxWait time.Duration) error { + sl.instanceMtx.Lock() + defer sl.instanceMtx.Unlock() + return sl.ensureConnection(maxWait) +} + +// SendRequest ensures there is a connection, sends a request and waits for a response +func (sl *SignerListenerEndpoint) SendRequest(request SignerMessage) (SignerMessage, error) { + sl.instanceMtx.Lock() + defer sl.instanceMtx.Unlock() + + err := sl.ensureConnection(sl.timeoutAccept) + if err != nil { + return nil, err + } + + err = sl.WriteMessage(request) + if err != nil { + return nil, err + } + + res, err := sl.ReadMessage() + if err != nil { + return nil, err + } + + return res, nil +} + +func (sl *SignerListenerEndpoint) ensureConnection(maxWait time.Duration) error { + if sl.IsConnected() { + return nil + } + + // Is there a connection ready? then use it + if sl.GetAvailableConnection(sl.connectionAvailableCh) { + return nil + } + + // block until connected or timeout + sl.triggerConnect() + err := sl.WaitConnection(sl.connectionAvailableCh, maxWait) + if err != nil { + return err + } + + return nil +} + +func (sl *SignerListenerEndpoint) acceptNewConnection() (net.Conn, error) { + if !sl.IsRunning() || sl.listener == nil { + return nil, fmt.Errorf("endpoint is closing") + } + + // wait for a new conn + sl.Logger.Info("SignerListener: Listening for new connection") + conn, err := sl.listener.Accept() + if err != nil { + return nil, err + } + + return conn, nil +} + +func (sl *SignerListenerEndpoint) triggerConnect() { + select { + case sl.connectRequestCh <- struct{}{}: + default: + } +} + +func (sl *SignerListenerEndpoint) triggerReconnect() { + sl.DropConnection() + sl.triggerConnect() +} + +func (sl *SignerListenerEndpoint) serviceLoop() { + for { + select { + case <-sl.connectRequestCh: + { + conn, err := sl.acceptNewConnection() + if err == nil { + sl.Logger.Info("SignerListener: Connected") + + // We have a good connection, wait for someone that needs one otherwise cancellation + select { + case sl.connectionAvailableCh <- conn: + case <-sl.Quit(): + return + } + } + + select { + case sl.connectRequestCh <- struct{}{}: + default: + } + } + case <-sl.Quit(): + return + } + } +} + +func (sl *SignerListenerEndpoint) pingLoop() { + for { + select { + case <-sl.pingTimer.C: + { + _, err := sl.SendRequest(&PingRequest{}) + if err != nil { + sl.Logger.Error("SignerListener: Ping timeout") + sl.triggerReconnect() + } + } + case <-sl.Quit(): + return + } + } +} diff --git a/privval/signer_listener_endpoint_test.go b/privval/signer_listener_endpoint_test.go new file mode 100644 index 000000000..7058ff8b8 --- /dev/null +++ b/privval/signer_listener_endpoint_test.go @@ -0,0 +1,198 @@ +package privval + +import ( + "net" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto/ed25519" + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/types" +) + +var ( + testTimeoutAccept = defaultTimeoutAcceptSeconds * time.Second + + testTimeoutReadWrite = 100 * time.Millisecond + testTimeoutReadWrite2o3 = 60 * time.Millisecond // 2/3 of the other one +) + +type dialerTestCase struct { + addr string + dialer SocketDialer +} + +// TestSignerRemoteRetryTCPOnly will test connection retry attempts over TCP. We +// don't need this for Unix sockets because the OS instantly knows the state of +// both ends of the socket connection. This basically causes the +// SignerDialerEndpoint.dialer() call inside SignerDialerEndpoint.acceptNewConnection() to return +// successfully immediately, putting an instant stop to any retry attempts. +func TestSignerRemoteRetryTCPOnly(t *testing.T) { + var ( + attemptCh = make(chan int) + retries = 10 + ) + + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + // Continuously Accept connection and close {attempts} times + go func(ln net.Listener, attemptCh chan<- int) { + attempts := 0 + for { + conn, err := ln.Accept() + require.NoError(t, err) + + err = conn.Close() + require.NoError(t, err) + + attempts++ + + if attempts == retries { + attemptCh <- attempts + break + } + } + }(ln, attemptCh) + + dialerEndpoint := NewSignerDialerEndpoint( + log.TestingLogger(), + DialTCPFn(ln.Addr().String(), testTimeoutReadWrite, ed25519.GenPrivKey()), + ) + SignerDialerEndpointTimeoutReadWrite(time.Millisecond)(dialerEndpoint) + SignerDialerEndpointConnRetries(retries)(dialerEndpoint) + + chainId := cmn.RandStr(12) + mockPV := types.NewMockPV() + signerServer := NewSignerServer(dialerEndpoint, chainId, mockPV) + + err = signerServer.Start() + require.NoError(t, err) + defer signerServer.Stop() + + select { + case attempts := <-attemptCh: + assert.Equal(t, retries, attempts) + case <-time.After(1500 * time.Millisecond): + t.Error("expected remote to observe connection attempts") + } +} + +func TestRetryConnToRemoteSigner(t *testing.T) { + for _, tc := range getDialerTestCases(t) { + var ( + logger = log.TestingLogger() + chainID = cmn.RandStr(12) + mockPV = types.NewMockPV() + endpointIsOpenCh = make(chan struct{}) + thisConnTimeout = testTimeoutReadWrite + listenerEndpoint = newSignerListenerEndpoint(logger, tc.addr, thisConnTimeout) + ) + + dialerEndpoint := NewSignerDialerEndpoint( + logger, + tc.dialer, + ) + SignerDialerEndpointTimeoutReadWrite(testTimeoutReadWrite)(dialerEndpoint) + SignerDialerEndpointConnRetries(10)(dialerEndpoint) + + signerServer := NewSignerServer(dialerEndpoint, chainID, mockPV) + + startListenerEndpointAsync(t, listenerEndpoint, endpointIsOpenCh) + defer listenerEndpoint.Stop() + + require.NoError(t, signerServer.Start()) + assert.True(t, signerServer.IsRunning()) + <-endpointIsOpenCh + signerServer.Stop() + + dialerEndpoint2 := NewSignerDialerEndpoint( + logger, + tc.dialer, + ) + signerServer2 := NewSignerServer(dialerEndpoint2, chainID, mockPV) + + // let some pings pass + require.NoError(t, signerServer2.Start()) + assert.True(t, signerServer2.IsRunning()) + defer signerServer2.Stop() + + // give the client some time to re-establish the conn to the remote signer + // should see sth like this in the logs: + // + // E[10016-01-10|17:12:46.128] Ping err="remote signer timed out" + // I[10016-01-10|17:16:42.447] Re-created connection to remote signer impl=SocketVal + time.Sleep(testTimeoutReadWrite * 2) + } +} + +/////////////////////////////////// + +func newSignerListenerEndpoint(logger log.Logger, addr string, timeoutReadWrite time.Duration) *SignerListenerEndpoint { + proto, address := cmn.ProtocolAndAddress(addr) + + ln, err := net.Listen(proto, address) + logger.Info("SignerListener: Listening", "proto", proto, "address", address) + if err != nil { + panic(err) + } + + var listener net.Listener + + if proto == "unix" { + unixLn := NewUnixListener(ln) + UnixListenerTimeoutAccept(testTimeoutAccept)(unixLn) + UnixListenerTimeoutReadWrite(timeoutReadWrite)(unixLn) + listener = unixLn + } else { + tcpLn := NewTCPListener(ln, ed25519.GenPrivKey()) + TCPListenerTimeoutAccept(testTimeoutAccept)(tcpLn) + TCPListenerTimeoutReadWrite(timeoutReadWrite)(tcpLn) + listener = tcpLn + } + + return NewSignerListenerEndpoint(logger, listener) +} + +func startListenerEndpointAsync(t *testing.T, sle *SignerListenerEndpoint, endpointIsOpenCh chan struct{}) { + go func(sle *SignerListenerEndpoint) { + require.NoError(t, sle.Start()) + assert.True(t, sle.IsRunning()) + close(endpointIsOpenCh) + }(sle) +} + +func getMockEndpoints( + t *testing.T, + addr string, + socketDialer SocketDialer, +) (*SignerListenerEndpoint, *SignerDialerEndpoint) { + + var ( + logger = log.TestingLogger() + endpointIsOpenCh = make(chan struct{}) + + dialerEndpoint = NewSignerDialerEndpoint( + logger, + socketDialer, + ) + + listenerEndpoint = newSignerListenerEndpoint(logger, addr, testTimeoutReadWrite) + ) + + SignerDialerEndpointTimeoutReadWrite(testTimeoutReadWrite)(dialerEndpoint) + SignerDialerEndpointConnRetries(1e6)(dialerEndpoint) + + startListenerEndpointAsync(t, listenerEndpoint, endpointIsOpenCh) + + require.NoError(t, dialerEndpoint.Start()) + assert.True(t, dialerEndpoint.IsRunning()) + + <-endpointIsOpenCh + + return listenerEndpoint, dialerEndpoint +} diff --git a/privval/signer_remote.go b/privval/signer_remote.go deleted file mode 100644 index 730c2c8c1..000000000 --- a/privval/signer_remote.go +++ /dev/null @@ -1,203 +0,0 @@ -package privval - -import ( - "fmt" - "io" - "net" - - "github.com/pkg/errors" - - "github.com/tendermint/tendermint/crypto" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/types" -) - -// SignerRemote implements PrivValidator. -// It uses a net.Conn to request signatures from an external process. -type SignerRemote struct { - conn net.Conn - - // memoized - address types.Address - consensusPubKey crypto.PubKey -} - -// Check that SignerRemote implements PrivValidator. -var _ types.PrivValidator = (*SignerRemote)(nil) - -// NewSignerRemote returns an instance of SignerRemote. -func NewSignerRemote(conn net.Conn) (*SignerRemote, error) { - - // retrieve and memoize the consensus public key once. - pubKey, err := getPubKey(conn) - if err != nil { - return nil, cmn.ErrorWrap(err, "error while retrieving public key for remote signer") - } - if pubKey == nil { - return &SignerRemote{ - conn: conn, - }, nil - } - return &SignerRemote{ - conn: conn, - consensusPubKey: pubKey, - address: pubKey.Address(), - }, nil -} - -// Close calls Close on the underlying net.Conn. -func (sc *SignerRemote) Close() error { - return sc.conn.Close() -} - -func (sc *SignerRemote) GetAddress() types.Address { - return sc.address -} - -// GetPubKey implements PrivValidator. -func (sc *SignerRemote) GetPubKey() crypto.PubKey { - return sc.consensusPubKey -} - -// not thread-safe (only called on startup). -func getPubKey(conn net.Conn) (crypto.PubKey, error) { - err := writeMsg(conn, &PubKeyRequest{}) - if err != nil { - return nil, err - } - - res, err := readMsg(conn) - if err != nil { - return nil, err - } - - pubKeyResp, ok := res.(*PubKeyResponse) - if !ok { - return nil, errors.Wrap(ErrUnexpectedResponse, "response is not PubKeyResponse") - } - - if pubKeyResp.Error != nil { - return nil, errors.Wrap(pubKeyResp.Error, "failed to get private validator's public key") - } - - return pubKeyResp.PubKey, nil -} - -// SignVote implements PrivValidator. -func (sc *SignerRemote) SignVote(chainID string, vote *types.Vote) error { - err := writeMsg(sc.conn, &SignVoteRequest{Vote: vote}) - if err != nil { - return err - } - - res, err := readMsg(sc.conn) - if err != nil { - return err - } - - resp, ok := res.(*SignedVoteResponse) - if !ok { - return ErrUnexpectedResponse - } - if resp.Error != nil { - return resp.Error - } - *vote = *resp.Vote - - return nil -} - -// SignProposal implements PrivValidator. -func (sc *SignerRemote) SignProposal(chainID string, proposal *types.Proposal) error { - err := writeMsg(sc.conn, &SignProposalRequest{Proposal: proposal}) - if err != nil { - return err - } - - res, err := readMsg(sc.conn) - if err != nil { - return err - } - resp, ok := res.(*SignedProposalResponse) - if !ok { - return ErrUnexpectedResponse - } - if resp.Error != nil { - return resp.Error - } - *proposal = *resp.Proposal - - return nil -} - -// Ping is used to check connection health. -func (sc *SignerRemote) Ping() error { - err := writeMsg(sc.conn, &PingRequest{}) - if err != nil { - return err - } - - res, err := readMsg(sc.conn) - if err != nil { - return err - } - _, ok := res.(*PingResponse) - if !ok { - return ErrUnexpectedResponse - } - - return nil -} - -func readMsg(r io.Reader) (msg RemoteSignerMsg, err error) { - const maxRemoteSignerMsgSize = 1024 * 10 - _, err = cdc.UnmarshalBinaryLengthPrefixedReader(r, &msg, maxRemoteSignerMsgSize) - if _, ok := err.(timeoutError); ok { - err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) - } - return -} - -func writeMsg(w io.Writer, msg interface{}) (err error) { - _, err = cdc.MarshalBinaryLengthPrefixedWriter(w, msg) - if _, ok := err.(timeoutError); ok { - err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) - } - return -} - -func handleRequest(req RemoteSignerMsg, chainID string, privVal types.PrivValidator) (RemoteSignerMsg, error) { - var res RemoteSignerMsg - var err error - - switch r := req.(type) { - case *PubKeyRequest: - var p crypto.PubKey - p = privVal.GetPubKey() - res = &PubKeyResponse{p, nil} - - case *SignVoteRequest: - err = privVal.SignVote(chainID, r.Vote) - if err != nil { - res = &SignedVoteResponse{nil, &RemoteSignerError{0, err.Error()}} - } else { - res = &SignedVoteResponse{r.Vote, nil} - } - - case *SignProposalRequest: - err = privVal.SignProposal(chainID, r.Proposal) - if err != nil { - res = &SignedProposalResponse{nil, &RemoteSignerError{0, err.Error()}} - } else { - res = &SignedProposalResponse{r.Proposal, nil} - } - - case *PingRequest: - res = &PingResponse{} - - default: - err = fmt.Errorf("unknown msg: %v", r) - } - - return res, err -} diff --git a/privval/signer_remote_test.go b/privval/signer_remote_test.go deleted file mode 100644 index 28230b803..000000000 --- a/privval/signer_remote_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package privval - -import ( - "net" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/tendermint/tendermint/crypto/ed25519" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/types" -) - -// TestSignerRemoteRetryTCPOnly will test connection retry attempts over TCP. We -// don't need this for Unix sockets because the OS instantly knows the state of -// both ends of the socket connection. This basically causes the -// SignerServiceEndpoint.dialer() call inside SignerServiceEndpoint.connect() to return -// successfully immediately, putting an instant stop to any retry attempts. -func TestSignerRemoteRetryTCPOnly(t *testing.T) { - var ( - attemptCh = make(chan int) - retries = 2 - ) - - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - - go func(ln net.Listener, attemptCh chan<- int) { - attempts := 0 - - for { - conn, err := ln.Accept() - require.NoError(t, err) - - err = conn.Close() - require.NoError(t, err) - - attempts++ - - if attempts == retries { - attemptCh <- attempts - break - } - } - }(ln, attemptCh) - - serviceEndpoint := NewSignerServiceEndpoint( - log.TestingLogger(), - cmn.RandStr(12), - types.NewMockPV(), - DialTCPFn(ln.Addr().String(), testTimeoutReadWrite, ed25519.GenPrivKey()), - ) - defer serviceEndpoint.Stop() - - SignerServiceEndpointTimeoutReadWrite(time.Millisecond)(serviceEndpoint) - SignerServiceEndpointConnRetries(retries)(serviceEndpoint) - - assert.Equal(t, serviceEndpoint.Start(), ErrDialRetryMax) - - select { - case attempts := <-attemptCh: - assert.Equal(t, retries, attempts) - case <-time.After(100 * time.Millisecond): - t.Error("expected remote to observe connection attempts") - } -} diff --git a/privval/signer_requestHandler.go b/privval/signer_requestHandler.go new file mode 100644 index 000000000..dcab7752e --- /dev/null +++ b/privval/signer_requestHandler.go @@ -0,0 +1,44 @@ +package privval + +import ( + "fmt" + + "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/types" +) + +func DefaultValidationRequestHandler(privVal types.PrivValidator, req SignerMessage, chainID string) (SignerMessage, error) { + var res SignerMessage + var err error + + switch r := req.(type) { + case *PubKeyRequest: + var p crypto.PubKey + p = privVal.GetPubKey() + res = &PubKeyResponse{p, nil} + + case *SignVoteRequest: + err = privVal.SignVote(chainID, r.Vote) + if err != nil { + res = &SignedVoteResponse{nil, &RemoteSignerError{0, err.Error()}} + } else { + res = &SignedVoteResponse{r.Vote, nil} + } + + case *SignProposalRequest: + err = privVal.SignProposal(chainID, r.Proposal) + if err != nil { + res = &SignedProposalResponse{nil, &RemoteSignerError{0, err.Error()}} + } else { + res = &SignedProposalResponse{r.Proposal, nil} + } + + case *PingRequest: + err, res = nil, &PingResponse{} + + default: + err = fmt.Errorf("unknown msg: %v", r) + } + + return res, err +} diff --git a/privval/signer_server.go b/privval/signer_server.go new file mode 100644 index 000000000..62dcc461c --- /dev/null +++ b/privval/signer_server.go @@ -0,0 +1,107 @@ +package privval + +import ( + "io" + "sync" + + cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/types" +) + +// ValidationRequestHandlerFunc handles different remoteSigner requests +type ValidationRequestHandlerFunc func( + privVal types.PrivValidator, + requestMessage SignerMessage, + chainID string) (SignerMessage, error) + +type SignerServer struct { + cmn.BaseService + + endpoint *SignerDialerEndpoint + chainID string + privVal types.PrivValidator + + handlerMtx sync.Mutex + validationRequestHandler ValidationRequestHandlerFunc +} + +func NewSignerServer(endpoint *SignerDialerEndpoint, chainID string, privVal types.PrivValidator) *SignerServer { + ss := &SignerServer{ + endpoint: endpoint, + chainID: chainID, + privVal: privVal, + validationRequestHandler: DefaultValidationRequestHandler, + } + + ss.BaseService = *cmn.NewBaseService(endpoint.Logger, "SignerServer", ss) + + return ss +} + +// OnStart implements cmn.Service. +func (ss *SignerServer) OnStart() error { + go ss.serviceLoop() + return nil +} + +// OnStop implements cmn.Service. +func (ss *SignerServer) OnStop() { + ss.endpoint.Logger.Debug("SignerServer: OnStop calling Close") + _ = ss.endpoint.Close() +} + +// SetRequestHandler override the default function that is used to service requests +func (ss *SignerServer) SetRequestHandler(validationRequestHandler ValidationRequestHandlerFunc) { + ss.handlerMtx.Lock() + defer ss.handlerMtx.Unlock() + ss.validationRequestHandler = validationRequestHandler +} + +func (ss *SignerServer) servicePendingRequest() { + if !ss.IsRunning() { + return // Ignore error from closing. + } + + req, err := ss.endpoint.ReadMessage() + if err != nil { + if err != io.EOF { + ss.Logger.Error("SignerServer: HandleMessage", "err", err) + } + return + } + + var res SignerMessage + { + // limit the scope of the lock + ss.handlerMtx.Lock() + defer ss.handlerMtx.Unlock() + res, err = ss.validationRequestHandler(ss.privVal, req, ss.chainID) + if err != nil { + // only log the error; we'll reply with an error in res + ss.Logger.Error("SignerServer: handleMessage", "err", err) + } + } + + if res != nil { + err = ss.endpoint.WriteMessage(res) + if err != nil { + ss.Logger.Error("SignerServer: writeMessage", "err", err) + } + } +} + +func (ss *SignerServer) serviceLoop() { + for { + select { + default: + err := ss.endpoint.ensureConnection() + if err != nil { + return + } + ss.servicePendingRequest() + + case <-ss.Quit(): + return + } + } +} diff --git a/privval/signer_service_endpoint.go b/privval/signer_service_endpoint.go deleted file mode 100644 index 1b37d5fc6..000000000 --- a/privval/signer_service_endpoint.go +++ /dev/null @@ -1,139 +0,0 @@ -package privval - -import ( - "io" - "net" - "time" - - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/types" -) - -// SignerServiceEndpointOption sets an optional parameter on the SignerServiceEndpoint. -type SignerServiceEndpointOption func(*SignerServiceEndpoint) - -// SignerServiceEndpointTimeoutReadWrite sets the read and write timeout for connections -// from external signing processes. -func SignerServiceEndpointTimeoutReadWrite(timeout time.Duration) SignerServiceEndpointOption { - return func(ss *SignerServiceEndpoint) { ss.timeoutReadWrite = timeout } -} - -// SignerServiceEndpointConnRetries sets the amount of attempted retries to connect. -func SignerServiceEndpointConnRetries(retries int) SignerServiceEndpointOption { - return func(ss *SignerServiceEndpoint) { ss.connRetries = retries } -} - -// SignerServiceEndpoint dials using its dialer and responds to any -// signature requests using its privVal. -type SignerServiceEndpoint struct { - cmn.BaseService - - chainID string - timeoutReadWrite time.Duration - connRetries int - privVal types.PrivValidator - - dialer SocketDialer - conn net.Conn -} - -// NewSignerServiceEndpoint returns a SignerServiceEndpoint that will dial using the given -// dialer and respond to any signature requests over the connection -// using the given privVal. -func NewSignerServiceEndpoint( - logger log.Logger, - chainID string, - privVal types.PrivValidator, - dialer SocketDialer, -) *SignerServiceEndpoint { - se := &SignerServiceEndpoint{ - chainID: chainID, - timeoutReadWrite: time.Second * defaultTimeoutReadWriteSeconds, - connRetries: defaultMaxDialRetries, - privVal: privVal, - dialer: dialer, - } - - se.BaseService = *cmn.NewBaseService(logger, "SignerServiceEndpoint", se) - return se -} - -// OnStart implements cmn.Service. -func (se *SignerServiceEndpoint) OnStart() error { - conn, err := se.connect() - if err != nil { - se.Logger.Error("OnStart", "err", err) - return err - } - - se.conn = conn - go se.handleConnection(conn) - - return nil -} - -// OnStop implements cmn.Service. -func (se *SignerServiceEndpoint) OnStop() { - if se.conn == nil { - return - } - - if err := se.conn.Close(); err != nil { - se.Logger.Error("OnStop", "err", cmn.ErrorWrap(err, "closing listener failed")) - } -} - -func (se *SignerServiceEndpoint) connect() (net.Conn, error) { - for retries := 0; retries < se.connRetries; retries++ { - // Don't sleep if it is the first retry. - if retries > 0 { - time.Sleep(se.timeoutReadWrite) - } - - conn, err := se.dialer() - if err == nil { - return conn, nil - } - - se.Logger.Error("dialing", "err", err) - } - - return nil, ErrDialRetryMax -} - -func (se *SignerServiceEndpoint) handleConnection(conn net.Conn) { - for { - if !se.IsRunning() { - return // Ignore error from listener closing. - } - - // Reset the connection deadline - deadline := time.Now().Add(se.timeoutReadWrite) - err := conn.SetDeadline(deadline) - if err != nil { - return - } - - req, err := readMsg(conn) - if err != nil { - if err != io.EOF { - se.Logger.Error("handleConnection readMsg", "err", err) - } - return - } - - res, err := handleRequest(req, se.chainID, se.privVal) - - if err != nil { - // only log the error; we'll reply with an error in res - se.Logger.Error("handleConnection handleRequest", "err", err) - } - - err = writeMsg(conn, res) - if err != nil { - se.Logger.Error("handleConnection writeMsg", "err", err) - return - } - } -} diff --git a/privval/signer_validator_endpoint.go b/privval/signer_validator_endpoint.go deleted file mode 100644 index 840364e90..000000000 --- a/privval/signer_validator_endpoint.go +++ /dev/null @@ -1,237 +0,0 @@ -package privval - -import ( - "fmt" - "net" - "sync" - "time" - - "github.com/tendermint/tendermint/crypto" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - "github.com/tendermint/tendermint/types" -) - -const ( - defaultHeartbeatSeconds = 2 - defaultMaxDialRetries = 10 -) - -var ( - heartbeatPeriod = time.Second * defaultHeartbeatSeconds -) - -// SignerValidatorEndpointOption sets an optional parameter on the SocketVal. -type SignerValidatorEndpointOption func(*SignerValidatorEndpoint) - -// SignerValidatorEndpointSetHeartbeat sets the period on which to check the liveness of the -// connected Signer connections. -func SignerValidatorEndpointSetHeartbeat(period time.Duration) SignerValidatorEndpointOption { - return func(sc *SignerValidatorEndpoint) { sc.heartbeatPeriod = period } -} - -// SocketVal implements PrivValidator. -// It listens for an external process to dial in and uses -// the socket to request signatures. -type SignerValidatorEndpoint struct { - cmn.BaseService - - listener net.Listener - - // ping - cancelPingCh chan struct{} - pingTicker *time.Ticker - heartbeatPeriod time.Duration - - // signer is mutable since it can be reset if the connection fails. - // failures are detected by a background ping routine. - // All messages are request/response, so we hold the mutex - // so only one request/response pair can happen at a time. - // Methods on the underlying net.Conn itself are already goroutine safe. - mtx sync.Mutex - - // TODO: Signer should encapsulate and hide the endpoint completely. Invert the relation - signer *SignerRemote -} - -// Check that SignerValidatorEndpoint implements PrivValidator. -var _ types.PrivValidator = (*SignerValidatorEndpoint)(nil) - -// NewSignerValidatorEndpoint returns an instance of SignerValidatorEndpoint. -func NewSignerValidatorEndpoint(logger log.Logger, listener net.Listener) *SignerValidatorEndpoint { - sc := &SignerValidatorEndpoint{ - listener: listener, - heartbeatPeriod: heartbeatPeriod, - } - - sc.BaseService = *cmn.NewBaseService(logger, "SignerValidatorEndpoint", sc) - - return sc -} - -//-------------------------------------------------------- -// Implement PrivValidator - -// GetAddress implements PrivValidator. -func (ve *SignerValidatorEndpoint) GetAddress() types.Address { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.GetAddress() -} - -// GetPubKey implements PrivValidator. -func (ve *SignerValidatorEndpoint) GetPubKey() crypto.PubKey { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.GetPubKey() -} - -// SignVote implements PrivValidator. -func (ve *SignerValidatorEndpoint) SignVote(chainID string, vote *types.Vote) error { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.SignVote(chainID, vote) -} - -// SignProposal implements PrivValidator. -func (ve *SignerValidatorEndpoint) SignProposal(chainID string, proposal *types.Proposal) error { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.SignProposal(chainID, proposal) -} - -//-------------------------------------------------------- -// More thread safe methods proxied to the signer - -// Ping is used to check connection health. -func (ve *SignerValidatorEndpoint) Ping() error { - ve.mtx.Lock() - defer ve.mtx.Unlock() - return ve.signer.Ping() -} - -// Close closes the underlying net.Conn. -func (ve *SignerValidatorEndpoint) Close() { - ve.mtx.Lock() - defer ve.mtx.Unlock() - if ve.signer != nil { - if err := ve.signer.Close(); err != nil { - ve.Logger.Error("OnStop", "err", err) - } - } - - if ve.listener != nil { - if err := ve.listener.Close(); err != nil { - ve.Logger.Error("OnStop", "err", err) - } - } -} - -//-------------------------------------------------------- -// Service start and stop - -// OnStart implements cmn.Service. -func (ve *SignerValidatorEndpoint) OnStart() error { - if closed, err := ve.reset(); err != nil { - ve.Logger.Error("OnStart", "err", err) - return err - } else if closed { - return fmt.Errorf("listener is closed") - } - - // Start a routine to keep the connection alive - ve.cancelPingCh = make(chan struct{}, 1) - ve.pingTicker = time.NewTicker(ve.heartbeatPeriod) - go func() { - for { - select { - case <-ve.pingTicker.C: - err := ve.Ping() - if err != nil { - ve.Logger.Error("Ping", "err", err) - if err == ErrUnexpectedResponse { - return - } - - closed, err := ve.reset() - if err != nil { - ve.Logger.Error("Reconnecting to remote signer failed", "err", err) - continue - } - if closed { - ve.Logger.Info("listener is closing") - return - } - - ve.Logger.Info("Re-created connection to remote signer", "impl", ve) - } - case <-ve.cancelPingCh: - ve.pingTicker.Stop() - return - } - } - }() - - return nil -} - -// OnStop implements cmn.Service. -func (ve *SignerValidatorEndpoint) OnStop() { - if ve.cancelPingCh != nil { - close(ve.cancelPingCh) - } - ve.Close() -} - -//-------------------------------------------------------- -// Connection and signer management - -// waits to accept and sets a new connection. -// connection is closed in OnStop. -// returns true if the listener is closed -// (ie. it returns a nil conn). -func (ve *SignerValidatorEndpoint) reset() (closed bool, err error) { - ve.mtx.Lock() - defer ve.mtx.Unlock() - - // first check if the conn already exists and close it. - if ve.signer != nil { - if tmpErr := ve.signer.Close(); tmpErr != nil { - ve.Logger.Error("error closing socket val connection during reset", "err", tmpErr) - } - } - - // wait for a new conn - conn, err := ve.acceptConnection() - if err != nil { - return false, err - } - - // listener is closed - if conn == nil { - return true, nil - } - - ve.signer, err = NewSignerRemote(conn) - if err != nil { - // failed to fetch the pubkey. close out the connection. - if tmpErr := conn.Close(); tmpErr != nil { - ve.Logger.Error("error closing connection", "err", tmpErr) - } - return false, err - } - return false, nil -} - -// Attempt to accept a connection. -// Times out after the listener's timeoutAccept -func (ve *SignerValidatorEndpoint) acceptConnection() (net.Conn, error) { - conn, err := ve.listener.Accept() - if err != nil { - if !ve.IsRunning() { - return nil, nil // Ignore error from listener closing. - } - return nil, err - } - return conn, nil -} diff --git a/privval/signer_validator_endpoint_test.go b/privval/signer_validator_endpoint_test.go deleted file mode 100644 index 611e743c9..000000000 --- a/privval/signer_validator_endpoint_test.go +++ /dev/null @@ -1,506 +0,0 @@ -package privval - -import ( - "fmt" - "net" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/tendermint/tendermint/crypto/ed25519" - cmn "github.com/tendermint/tendermint/libs/common" - "github.com/tendermint/tendermint/libs/log" - - "github.com/tendermint/tendermint/types" -) - -var ( - testTimeoutAccept = defaultTimeoutAcceptSeconds * time.Second - - testTimeoutReadWrite = 100 * time.Millisecond - testTimeoutReadWrite2o3 = 66 * time.Millisecond // 2/3 of the other one - - testTimeoutHeartbeat = 10 * time.Millisecond - testTimeoutHeartbeat3o2 = 6 * time.Millisecond // 3/2 of the other one -) - -type socketTestCase struct { - addr string - dialer SocketDialer -} - -func socketTestCases(t *testing.T) []socketTestCase { - tcpAddr := fmt.Sprintf("tcp://%s", testFreeTCPAddr(t)) - unixFilePath, err := testUnixAddr() - require.NoError(t, err) - unixAddr := fmt.Sprintf("unix://%s", unixFilePath) - return []socketTestCase{ - { - addr: tcpAddr, - dialer: DialTCPFn(tcpAddr, testTimeoutReadWrite, ed25519.GenPrivKey()), - }, - { - addr: unixAddr, - dialer: DialUnixFn(unixFilePath), - }, - } -} - -func TestSocketPVAddress(t *testing.T) { - for _, tc := range socketTestCases(t) { - // Execute the test within a closure to ensure the deferred statements - // are called between each for loop iteration, for isolated test cases. - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair(t, chainID, types.NewMockPV(), tc.addr, tc.dialer) - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - serviceAddr := serviceEndpoint.privVal.GetPubKey().Address() - validatorAddr := validatorEndpoint.GetPubKey().Address() - - assert.Equal(t, serviceAddr, validatorAddr) - }() - } -} - -func TestSocketPVPubKey(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - clientKey := validatorEndpoint.GetPubKey() - privvalPubKey := serviceEndpoint.privVal.GetPubKey() - - assert.Equal(t, privvalPubKey, clientKey) - }() - } -} - -func TestSocketPVProposal(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - privProposal = &types.Proposal{Timestamp: ts} - clientProposal = &types.Proposal{Timestamp: ts} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - require.NoError(t, serviceEndpoint.privVal.SignProposal(chainID, privProposal)) - require.NoError(t, validatorEndpoint.SignProposal(chainID, clientProposal)) - - assert.Equal(t, privProposal.Signature, clientProposal.Signature) - }() - } -} - -func TestSocketPVVote(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - want = &types.Vote{Timestamp: ts, Type: vType} - have = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - }() - } -} - -func TestSocketPVVoteResetDeadline(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - want = &types.Vote{Timestamp: ts, Type: vType} - have = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - time.Sleep(testTimeoutReadWrite2o3) - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - - // This would exceed the deadline if it was not extended by the previous message - time.Sleep(testTimeoutReadWrite2o3) - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - }() - } -} - -func TestSocketPVVoteKeepalive(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - want = &types.Vote{Timestamp: ts, Type: vType} - have = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - time.Sleep(testTimeoutReadWrite * 2) - - require.NoError(t, serviceEndpoint.privVal.SignVote(chainID, want)) - require.NoError(t, validatorEndpoint.SignVote(chainID, have)) - assert.Equal(t, want.Signature, have.Signature) - }() - } -} - -func TestSocketPVDeadline(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - listenc = make(chan struct{}) - thisConnTimeout = 100 * time.Millisecond - validatorEndpoint = newSignerValidatorEndpoint(log.TestingLogger(), tc.addr, thisConnTimeout) - ) - - go func(sc *SignerValidatorEndpoint) { - defer close(listenc) - - // Note: the TCP connection times out at the accept() phase, - // whereas the Unix domain sockets connection times out while - // attempting to fetch the remote signer's public key. - assert.True(t, IsConnTimeout(sc.Start())) - - assert.False(t, sc.IsRunning()) - }(validatorEndpoint) - - for { - _, err := cmn.Connect(tc.addr) - if err == nil { - break - } - } - - <-listenc - }() - } -} - -func TestRemoteSignVoteErrors(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewErroringMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - vType = types.PrecommitType - vote = &types.Vote{Timestamp: ts, Type: vType} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - err := validatorEndpoint.SignVote("", vote) - require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) - - err = serviceEndpoint.privVal.SignVote(chainID, vote) - require.Error(t, err) - err = validatorEndpoint.SignVote(chainID, vote) - require.Error(t, err) - }() - } -} - -func TestRemoteSignProposalErrors(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - chainID = cmn.RandStr(12) - validatorEndpoint, serviceEndpoint = testSetupSocketPair( - t, - chainID, - types.NewErroringMockPV(), - tc.addr, - tc.dialer) - - ts = time.Now() - proposal = &types.Proposal{Timestamp: ts} - ) - defer validatorEndpoint.Stop() - defer serviceEndpoint.Stop() - - err := validatorEndpoint.SignProposal("", proposal) - require.Equal(t, err.(*RemoteSignerError).Description, types.ErroringMockPVErr.Error()) - - err = serviceEndpoint.privVal.SignProposal(chainID, proposal) - require.Error(t, err) - - err = validatorEndpoint.SignProposal(chainID, proposal) - require.Error(t, err) - }() - } -} - -func TestErrUnexpectedResponse(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - logger = log.TestingLogger() - chainID = cmn.RandStr(12) - readyCh = make(chan struct{}) - errCh = make(chan error, 1) - - serviceEndpoint = NewSignerServiceEndpoint( - logger, - chainID, - types.NewMockPV(), - tc.dialer, - ) - - validatorEndpoint = newSignerValidatorEndpoint( - logger, - tc.addr, - testTimeoutReadWrite) - ) - - testStartEndpoint(t, readyCh, validatorEndpoint) - defer validatorEndpoint.Stop() - SignerServiceEndpointTimeoutReadWrite(time.Millisecond)(serviceEndpoint) - SignerServiceEndpointConnRetries(100)(serviceEndpoint) - // we do not want to Start() the remote signer here and instead use the connection to - // reply with intentionally wrong replies below: - rsConn, err := serviceEndpoint.connect() - require.NoError(t, err) - require.NotNil(t, rsConn) - defer rsConn.Close() - - // send over public key to get the remote signer running: - go testReadWriteResponse(t, &PubKeyResponse{}, rsConn) - <-readyCh - - // Proposal: - go func(errc chan error) { - errc <- validatorEndpoint.SignProposal(chainID, &types.Proposal{}) - }(errCh) - - // read request and write wrong response: - go testReadWriteResponse(t, &SignedVoteResponse{}, rsConn) - err = <-errCh - require.Error(t, err) - require.Equal(t, err, ErrUnexpectedResponse) - - // Vote: - go func(errc chan error) { - errc <- validatorEndpoint.SignVote(chainID, &types.Vote{}) - }(errCh) - // read request and write wrong response: - go testReadWriteResponse(t, &SignedProposalResponse{}, rsConn) - err = <-errCh - require.Error(t, err) - require.Equal(t, err, ErrUnexpectedResponse) - }() - } -} - -func TestRetryConnToRemoteSigner(t *testing.T) { - for _, tc := range socketTestCases(t) { - func() { - var ( - logger = log.TestingLogger() - chainID = cmn.RandStr(12) - readyCh = make(chan struct{}) - - serviceEndpoint = NewSignerServiceEndpoint( - logger, - chainID, - types.NewMockPV(), - tc.dialer, - ) - thisConnTimeout = testTimeoutReadWrite - validatorEndpoint = newSignerValidatorEndpoint(logger, tc.addr, thisConnTimeout) - ) - // Ping every: - SignerValidatorEndpointSetHeartbeat(testTimeoutHeartbeat)(validatorEndpoint) - - SignerServiceEndpointTimeoutReadWrite(testTimeoutReadWrite)(serviceEndpoint) - SignerServiceEndpointConnRetries(10)(serviceEndpoint) - - testStartEndpoint(t, readyCh, validatorEndpoint) - defer validatorEndpoint.Stop() - require.NoError(t, serviceEndpoint.Start()) - assert.True(t, serviceEndpoint.IsRunning()) - - <-readyCh - time.Sleep(testTimeoutHeartbeat * 2) - - serviceEndpoint.Stop() - rs2 := NewSignerServiceEndpoint( - logger, - chainID, - types.NewMockPV(), - tc.dialer, - ) - // let some pings pass - time.Sleep(testTimeoutHeartbeat3o2) - require.NoError(t, rs2.Start()) - assert.True(t, rs2.IsRunning()) - defer rs2.Stop() - - // give the client some time to re-establish the conn to the remote signer - // should see sth like this in the logs: - // - // E[10016-01-10|17:12:46.128] Ping err="remote signer timed out" - // I[10016-01-10|17:16:42.447] Re-created connection to remote signer impl=SocketVal - time.Sleep(testTimeoutReadWrite * 2) - }() - } -} - -func newSignerValidatorEndpoint(logger log.Logger, addr string, timeoutReadWrite time.Duration) *SignerValidatorEndpoint { - proto, address := cmn.ProtocolAndAddress(addr) - - ln, err := net.Listen(proto, address) - logger.Info("Listening at", "proto", proto, "address", address) - if err != nil { - panic(err) - } - - var listener net.Listener - - if proto == "unix" { - unixLn := NewUnixListener(ln) - UnixListenerTimeoutAccept(testTimeoutAccept)(unixLn) - UnixListenerTimeoutReadWrite(timeoutReadWrite)(unixLn) - listener = unixLn - } else { - tcpLn := NewTCPListener(ln, ed25519.GenPrivKey()) - TCPListenerTimeoutAccept(testTimeoutAccept)(tcpLn) - TCPListenerTimeoutReadWrite(timeoutReadWrite)(tcpLn) - listener = tcpLn - } - - return NewSignerValidatorEndpoint(logger, listener) -} - -func testSetupSocketPair( - t *testing.T, - chainID string, - privValidator types.PrivValidator, - addr string, - socketDialer SocketDialer, -) (*SignerValidatorEndpoint, *SignerServiceEndpoint) { - var ( - logger = log.TestingLogger() - privVal = privValidator - readyc = make(chan struct{}) - serviceEndpoint = NewSignerServiceEndpoint( - logger, - chainID, - privVal, - socketDialer, - ) - - thisConnTimeout = testTimeoutReadWrite - validatorEndpoint = newSignerValidatorEndpoint(logger, addr, thisConnTimeout) - ) - - SignerValidatorEndpointSetHeartbeat(testTimeoutHeartbeat)(validatorEndpoint) - SignerServiceEndpointTimeoutReadWrite(testTimeoutReadWrite)(serviceEndpoint) - SignerServiceEndpointConnRetries(1e6)(serviceEndpoint) - - testStartEndpoint(t, readyc, validatorEndpoint) - - require.NoError(t, serviceEndpoint.Start()) - assert.True(t, serviceEndpoint.IsRunning()) - - <-readyc - - return validatorEndpoint, serviceEndpoint -} - -func testReadWriteResponse(t *testing.T, resp RemoteSignerMsg, rsConn net.Conn) { - _, err := readMsg(rsConn) - require.NoError(t, err) - - err = writeMsg(rsConn, resp) - require.NoError(t, err) -} - -func testStartEndpoint(t *testing.T, readyCh chan struct{}, sc *SignerValidatorEndpoint) { - go func(sc *SignerValidatorEndpoint) { - require.NoError(t, sc.Start()) - assert.True(t, sc.IsRunning()) - - readyCh <- struct{}{} - }(sc) -} - -// testFreeTCPAddr claims a free port so we don't block on listener being ready. -func testFreeTCPAddr(t *testing.T) string { - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer ln.Close() - - return fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port) -} diff --git a/privval/socket_dialers_test.go b/privval/socket_dialers_test.go index 9d5d5cc2b..c77261bc5 100644 --- a/privval/socket_dialers_test.go +++ b/privval/socket_dialers_test.go @@ -1,26 +1,49 @@ package privval import ( + "fmt" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" ) +func getDialerTestCases(t *testing.T) []dialerTestCase { + tcpAddr := GetFreeLocalhostAddrPort() + unixFilePath, err := testUnixAddr() + require.NoError(t, err) + unixAddr := fmt.Sprintf("unix://%s", unixFilePath) + + return []dialerTestCase{ + { + addr: tcpAddr, + dialer: DialTCPFn(tcpAddr, testTimeoutReadWrite, ed25519.GenPrivKey()), + }, + { + addr: unixAddr, + dialer: DialUnixFn(unixFilePath), + }, + } +} + func TestIsConnTimeoutForFundamentalTimeouts(t *testing.T) { // Generate a networking timeout - dialer := DialTCPFn(testFreeTCPAddr(t), time.Millisecond, ed25519.GenPrivKey()) + tcpAddr := GetFreeLocalhostAddrPort() + dialer := DialTCPFn(tcpAddr, time.Millisecond, ed25519.GenPrivKey()) _, err := dialer() assert.Error(t, err) assert.True(t, IsConnTimeout(err)) } func TestIsConnTimeoutForWrappedConnTimeouts(t *testing.T) { - dialer := DialTCPFn(testFreeTCPAddr(t), time.Millisecond, ed25519.GenPrivKey()) + tcpAddr := GetFreeLocalhostAddrPort() + dialer := DialTCPFn(tcpAddr, time.Millisecond, ed25519.GenPrivKey()) _, err := dialer() assert.Error(t, err) - err = cmn.ErrorWrap(ErrConnTimeout, err.Error()) + err = cmn.ErrorWrap(ErrConnectionTimeout, err.Error()) assert.True(t, IsConnTimeout(err)) } diff --git a/privval/socket_listeners.go b/privval/socket_listeners.go index 7c8835791..f4d875e71 100644 --- a/privval/socket_listeners.go +++ b/privval/socket_listeners.go @@ -9,8 +9,8 @@ import ( ) const ( - defaultTimeoutAcceptSeconds = 3 - defaultTimeoutReadWriteSeconds = 3 + defaultTimeoutAcceptSeconds = 3 + defaultPingPeriodMilliseconds = 100 ) // timeoutError can be used to check if an error returned from the netp package diff --git a/privval/utils.go b/privval/utils.go index d8837bdf0..a707e2ee4 100644 --- a/privval/utils.go +++ b/privval/utils.go @@ -1,7 +1,12 @@ package privval import ( + "fmt" + "net" + + "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" + "github.com/tendermint/tendermint/libs/log" ) // IsConnTimeout returns a boolean indicating whether the error is known to @@ -9,7 +14,7 @@ import ( // network timeouts, as well as ErrConnTimeout errors. func IsConnTimeout(err error) bool { if cmnErr, ok := err.(cmn.Error); ok { - if cmnErr.Data() == ErrConnTimeout { + if cmnErr.Data() == ErrConnectionTimeout { return true } } @@ -18,3 +23,39 @@ func IsConnTimeout(err error) bool { } return false } + +// NewSignerListener creates a new SignerListenerEndpoint using the corresponding listen address +func NewSignerListener(listenAddr string, logger log.Logger) (*SignerListenerEndpoint, error) { + var listener net.Listener + + protocol, address := cmn.ProtocolAndAddress(listenAddr) + ln, err := net.Listen(protocol, address) + if err != nil { + return nil, err + } + switch protocol { + case "unix": + listener = NewUnixListener(ln) + case "tcp": + // TODO: persist this key so external signer can actually authenticate us + listener = NewTCPListener(ln, ed25519.GenPrivKey()) + default: + return nil, fmt.Errorf( + "wrong listen address: expected either 'tcp' or 'unix' protocols, got %s", + protocol, + ) + } + + pve := NewSignerListenerEndpoint(logger.With("module", "privval"), listener) + + return pve, nil +} + +// GetFreeLocalhostAddrPort returns a free localhost:port address +func GetFreeLocalhostAddrPort() string { + port, err := cmn.GetFreePort() + if err != nil { + panic(err) + } + return fmt.Sprintf("127.0.0.1:%d", port) +} diff --git a/privval/utils_test.go b/privval/utils_test.go index 23f6f6a3b..b07186f6c 100644 --- a/privval/utils_test.go +++ b/privval/utils_test.go @@ -5,6 +5,7 @@ import ( "testing" "github.com/stretchr/testify/assert" + cmn "github.com/tendermint/tendermint/libs/common" ) diff --git a/tools/tm-signer-harness/internal/test_harness.go b/tools/tm-signer-harness/internal/test_harness.go index 7fefdfb42..216cf6851 100644 --- a/tools/tm-signer-harness/internal/test_harness.go +++ b/tools/tm-signer-harness/internal/test_harness.go @@ -49,7 +49,7 @@ var _ error = (*TestHarnessError)(nil) // with this version of Tendermint. type TestHarness struct { addr string - spv *privval.SignerValidatorEndpoint + signerClient *privval.SignerClient fpv *privval.FilePV chainID string acceptRetries int @@ -101,14 +101,19 @@ func NewTestHarness(logger log.Logger, cfg TestHarnessConfig) (*TestHarness, err } logger.Info("Loaded genesis file", "chainID", st.ChainID) - spv, err := newTestHarnessSocketVal(logger, cfg) + spv, err := newTestHarnessListener(logger, cfg) + if err != nil { + return nil, newTestHarnessError(ErrFailedToCreateListener, err, "") + } + + signerClient, err := privval.NewSignerClient(spv) if err != nil { return nil, newTestHarnessError(ErrFailedToCreateListener, err, "") } return &TestHarness{ addr: cfg.BindAddr, - spv: spv, + signerClient: signerClient, fpv: fpv, chainID: st.ChainID, acceptRetries: cfg.AcceptRetries, @@ -135,9 +140,11 @@ func (th *TestHarness) Run() { th.logger.Info("Starting test harness") accepted := false var startErr error + for acceptRetries := th.acceptRetries; acceptRetries > 0; acceptRetries-- { th.logger.Info("Attempting to accept incoming connection", "acceptRetries", acceptRetries) - if err := th.spv.Start(); err != nil { + + if err := th.signerClient.WaitForConnection(10 * time.Millisecond); err != nil { // if it wasn't a timeout error if _, ok := err.(timeoutError); !ok { th.logger.Error("Failed to start listener", "err", err) @@ -149,6 +156,7 @@ func (th *TestHarness) Run() { } startErr = err } else { + th.logger.Info("Accepted external connection") accepted = true break } @@ -182,8 +190,8 @@ func (th *TestHarness) Run() { func (th *TestHarness) TestPublicKey() error { th.logger.Info("TEST: Public key of remote signer") th.logger.Info("Local", "pubKey", th.fpv.GetPubKey()) - th.logger.Info("Remote", "pubKey", th.spv.GetPubKey()) - if th.fpv.GetPubKey() != th.spv.GetPubKey() { + th.logger.Info("Remote", "pubKey", th.signerClient.GetPubKey()) + if th.fpv.GetPubKey() != th.signerClient.GetPubKey() { th.logger.Error("FAILED: Local and remote public keys do not match") return newTestHarnessError(ErrTestPublicKeyFailed, nil, "") } @@ -211,7 +219,7 @@ func (th *TestHarness) TestSignProposal() error { Timestamp: time.Now(), } propBytes := prop.SignBytes(th.chainID) - if err := th.spv.SignProposal(th.chainID, prop); err != nil { + if err := th.signerClient.SignProposal(th.chainID, prop); err != nil { th.logger.Error("FAILED: Signing of proposal", "err", err) return newTestHarnessError(ErrTestSignProposalFailed, err, "") } @@ -222,7 +230,7 @@ func (th *TestHarness) TestSignProposal() error { return newTestHarnessError(ErrTestSignProposalFailed, err, "") } // now validate the signature on the proposal - if th.spv.GetPubKey().VerifyBytes(propBytes, prop.Signature) { + if th.signerClient.GetPubKey().VerifyBytes(propBytes, prop.Signature) { th.logger.Info("Successfully validated proposal signature") } else { th.logger.Error("FAILED: Proposal signature validation failed") @@ -255,7 +263,7 @@ func (th *TestHarness) TestSignVote() error { } voteBytes := vote.SignBytes(th.chainID) // sign the vote - if err := th.spv.SignVote(th.chainID, vote); err != nil { + if err := th.signerClient.SignVote(th.chainID, vote); err != nil { th.logger.Error("FAILED: Signing of vote", "err", err) return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType)) } @@ -266,7 +274,7 @@ func (th *TestHarness) TestSignVote() error { return newTestHarnessError(ErrTestSignVoteFailed, err, fmt.Sprintf("voteType=%d", voteType)) } // now validate the signature on the proposal - if th.spv.GetPubKey().VerifyBytes(voteBytes, vote.Signature) { + if th.signerClient.GetPubKey().VerifyBytes(voteBytes, vote.Signature) { th.logger.Info("Successfully validated vote signature", "type", voteType) } else { th.logger.Error("FAILED: Vote signature validation failed", "type", voteType) @@ -301,10 +309,9 @@ func (th *TestHarness) Shutdown(err error) { }() } - if th.spv.IsRunning() { - if err := th.spv.Stop(); err != nil { - th.logger.Error("Failed to cleanly stop listener: %s", err.Error()) - } + err = th.signerClient.Close() + if err != nil { + th.logger.Error("Failed to cleanly stop listener: %s", err.Error()) } if th.exitWhenComplete { @@ -312,9 +319,8 @@ func (th *TestHarness) Shutdown(err error) { } } -// newTestHarnessSocketVal creates our client instance which we will use for -// testing. -func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerValidatorEndpoint, error) { +// newTestHarnessListener creates our client instance which we will use for testing. +func newTestHarnessListener(logger log.Logger, cfg TestHarnessConfig) (*privval.SignerListenerEndpoint, error) { proto, addr := cmn.ProtocolAndAddress(cfg.BindAddr) if proto == "unix" { // make sure the socket doesn't exist - if so, try to delete it @@ -329,7 +335,7 @@ func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval if err != nil { return nil, err } - logger.Info("Listening at", "proto", proto, "addr", addr) + logger.Info("Listening", "proto", proto, "addr", addr) var svln net.Listener switch proto { case "unix": @@ -347,7 +353,7 @@ func newTestHarnessSocketVal(logger log.Logger, cfg TestHarnessConfig) (*privval logger.Error("Unsupported protocol (must be unix:// or tcp://)", "proto", proto) return nil, newTestHarnessError(ErrInvalidParameters, nil, fmt.Sprintf("Unsupported protocol: %s", proto)) } - return privval.NewSignerValidatorEndpoint(logger, svln), nil + return privval.NewSignerListenerEndpoint(logger, svln), nil } func newTestHarnessError(code int, err error, info string) *TestHarnessError { diff --git a/tools/tm-signer-harness/internal/test_harness_test.go b/tools/tm-signer-harness/internal/test_harness_test.go index adb818b0b..9d444ef5f 100644 --- a/tools/tm-signer-harness/internal/test_harness_test.go +++ b/tools/tm-signer-harness/internal/test_harness_test.go @@ -3,19 +3,18 @@ package internal import ( "fmt" "io/ioutil" - "net" "os" "testing" "time" - "github.com/tendermint/tendermint/crypto" - "github.com/tendermint/tendermint/privval" - "github.com/tendermint/tendermint/types" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/privval" + "github.com/tendermint/tendermint/types" ) const ( @@ -84,8 +83,8 @@ func TestRemoteSignerTestHarnessMaxAcceptRetriesReached(t *testing.T) { func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, false, false) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, false) }, NoError, ) @@ -94,8 +93,8 @@ func TestRemoteSignerTestHarnessSuccessfulRun(t *testing.T) { func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, ed25519.GenPrivKey(), false, false) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, ed25519.GenPrivKey(), false, false) }, ErrTestPublicKeyFailed, ) @@ -104,8 +103,8 @@ func TestRemoteSignerPublicKeyCheckFailed(t *testing.T) { func TestRemoteSignerProposalSigningFailed(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, true, false) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, th.fpv.Key.PrivKey, true, false) }, ErrTestSignProposalFailed, ) @@ -114,28 +113,30 @@ func TestRemoteSignerProposalSigningFailed(t *testing.T) { func TestRemoteSignerVoteSigningFailed(t *testing.T) { harnessTest( t, - func(th *TestHarness) *privval.SignerServiceEndpoint { - return newMockRemoteSigner(t, th, th.fpv.Key.PrivKey, false, true) + func(th *TestHarness) *privval.SignerServer { + return newMockSignerServer(t, th, th.fpv.Key.PrivKey, false, true) }, ErrTestSignVoteFailed, ) } -func newMockRemoteSigner(t *testing.T, th *TestHarness, privKey crypto.PrivKey, breakProposalSigning bool, breakVoteSigning bool) *privval.SignerServiceEndpoint { - return privval.NewSignerServiceEndpoint( +func newMockSignerServer(t *testing.T, th *TestHarness, privKey crypto.PrivKey, breakProposalSigning bool, breakVoteSigning bool) *privval.SignerServer { + mockPV := types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning) + + dialerEndpoint := privval.NewSignerDialerEndpoint( th.logger, - th.chainID, - types.NewMockPVWithParams(privKey, breakProposalSigning, breakVoteSigning), privval.DialTCPFn( th.addr, time.Duration(defaultConnDeadline)*time.Millisecond, ed25519.GenPrivKey(), ), ) + + return privval.NewSignerServer(dialerEndpoint, th.chainID, mockPV) } // For running relatively standard tests. -func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServiceEndpoint, expectedExitCode int) { +func harnessTest(t *testing.T, signerServerMaker func(th *TestHarness) *privval.SignerServer, expectedExitCode int) { cfg := makeConfig(t, 100, 3) defer cleanup(cfg) @@ -147,10 +148,10 @@ func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServ th.Run() }() - rs := rsMaker(th) - require.NoError(t, rs.Start()) - assert.True(t, rs.IsRunning()) - defer rs.Stop() + ss := signerServerMaker(th) + require.NoError(t, ss.Start()) + assert.True(t, ss.IsRunning()) + defer ss.Stop() <-donec assert.Equal(t, expectedExitCode, th.exitCode) @@ -158,7 +159,7 @@ func harnessTest(t *testing.T, rsMaker func(th *TestHarness) *privval.SignerServ func makeConfig(t *testing.T, acceptDeadline, acceptRetries int) TestHarnessConfig { return TestHarnessConfig{ - BindAddr: testFreeTCPAddr(t), + BindAddr: privval.GetFreeLocalhostAddrPort(), KeyFile: makeTempFile("tm-testharness-keyfile", keyFileContents), StateFile: makeTempFile("tm-testharness-statefile", stateFileContents), GenesisFile: makeTempFile("tm-testharness-genesisfile", genesisFileContents), @@ -190,12 +191,3 @@ func makeTempFile(name, content string) string { } return tempFile.Name() } - -// testFreeTCPAddr claims a free port so we don't block on listener being ready. -func testFreeTCPAddr(t *testing.T) string { - ln, err := net.Listen("tcp", "127.0.0.1:0") - require.NoError(t, err) - defer ln.Close() - - return fmt.Sprintf("127.0.0.1:%d", ln.Addr().(*net.TCPAddr).Port) -} diff --git a/types/priv_validator.go b/types/priv_validator.go index ca0e5ed15..ab5e8ef1b 100644 --- a/types/priv_validator.go +++ b/types/priv_validator.go @@ -11,7 +11,7 @@ import ( // PrivValidator defines the functionality of a local Tendermint validator // that signs votes and proposals, and never double signs. type PrivValidator interface { - GetAddress() Address + // TODO: Extend the interface to return errors too. Issue: https://github.com/tendermint/tendermint/issues/3602 GetPubKey() crypto.PubKey SignVote(chainID string, vote *Vote) error @@ -28,7 +28,7 @@ func (pvs PrivValidatorsByAddress) Len() int { } func (pvs PrivValidatorsByAddress) Less(i, j int) bool { - return bytes.Compare(pvs[i].GetAddress(), pvs[j].GetAddress()) == -1 + return bytes.Compare(pvs[i].GetPubKey().Address(), pvs[j].GetPubKey().Address()) == -1 } func (pvs PrivValidatorsByAddress) Swap(i, j int) { From 226b5c378a68fee4d57e20ac1d80558c14607f0d Mon Sep 17 00:00:00 2001 From: Sean Braithwaite Date: Mon, 5 Aug 2019 17:26:46 +0200 Subject: [PATCH 169/211] [blockchain] v2 riri Schedule composit data structure (#3848) * Add Schedule: + The schedule is a data structure used to determine the optimal schedule of requests to the optimal set of peers in order to perform fast-sync as fast and efficiently as possible. * Add some doc strings * fix golangci * Add Schedule: + The schedule is a data structure used to determine the optimal schedule of requests to the optimal set of peers in order to perform fast-sync as fast and efficiently as possible. * Add some doc strings * remove globals from tests --- blockchain/v2/schedule.go | 387 +++++++++++++++++++++++++++++++++ blockchain/v2/schedule_test.go | 272 +++++++++++++++++++++++ 2 files changed, 659 insertions(+) create mode 100644 blockchain/v2/schedule.go create mode 100644 blockchain/v2/schedule_test.go diff --git a/blockchain/v2/schedule.go b/blockchain/v2/schedule.go new file mode 100644 index 000000000..329557492 --- /dev/null +++ b/blockchain/v2/schedule.go @@ -0,0 +1,387 @@ +// nolint:unused +package v2 + +import ( + "fmt" + "math" + "math/rand" + "time" + + "github.com/tendermint/tendermint/p2p" +) + +type Event interface{} + +type blockState int + +const ( + blockStateUnknown blockState = iota + blockStateNew + blockStatePending + blockStateReceived + blockStateProcessed +) + +func (e blockState) String() string { + switch e { + case blockStateUnknown: + return "Unknown" + case blockStateNew: + return "New" + case blockStatePending: + return "Pending" + case blockStateReceived: + return "Received" + case blockStateProcessed: + return "Processed" + default: + return fmt.Sprintf("unknown blockState: %d", e) + } +} + +type peerState int + +const ( + peerStateNew = iota + peerStateReady + peerStateRemoved +) + +func (e peerState) String() string { + switch e { + case peerStateNew: + return "New" + case peerStateReady: + return "Ready" + case peerStateRemoved: + return "Removed" + default: + return fmt.Sprintf("unknown peerState: %d", e) + } +} + +type scPeer struct { + peerID p2p.ID + state peerState + height int64 + lastTouched time.Time + lastRate int64 +} + +func newScPeer(peerID p2p.ID) *scPeer { + return &scPeer{ + peerID: peerID, + state: peerStateNew, + height: -1, + lastTouched: time.Time{}, + } +} + +// The schedule is a composite data structure which allows a scheduler to keep +// track of which blocks have been scheduled into which state. +type schedule struct { + initHeight int64 + // a list of blocks in which blockState + blockStates map[int64]blockState + + // a map of peerID to schedule specific peer struct `scPeer` used to keep + // track of peer specific state + peers map[p2p.ID]*scPeer + + // a map of heights to the peer we are waiting for a response from + pendingBlocks map[int64]p2p.ID + + // the time at which a block was put in blockStatePending + pendingTime map[int64]time.Time + + // the peerID of the peer which put the block in blockStateReceived + receivedBlocks map[int64]p2p.ID +} + +func newSchedule(initHeight int64) *schedule { + sc := schedule{ + initHeight: initHeight, + blockStates: make(map[int64]blockState), + peers: make(map[p2p.ID]*scPeer), + pendingBlocks: make(map[int64]p2p.ID), + pendingTime: make(map[int64]time.Time), + receivedBlocks: make(map[int64]p2p.ID), + } + + sc.setStateAtHeight(initHeight, blockStateNew) + + return &sc +} + +func (sc *schedule) addPeer(peerID p2p.ID) error { + if _, ok := sc.peers[peerID]; ok { + return fmt.Errorf("Cannot add duplicate peer %s", peerID) + } + sc.peers[peerID] = newScPeer(peerID) + return nil +} + +func (sc *schedule) touchPeer(peerID p2p.ID, time time.Time) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Couldn't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Tried to touch peer in peerStateRemoved") + } + + peer.lastTouched = time + + return nil +} + +func (sc *schedule) removePeer(peerID p2p.ID) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Couldn't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Tried to remove peer %s in peerStateRemoved", peerID) + } + + for height, pendingPeerID := range sc.pendingBlocks { + if pendingPeerID == peerID { + sc.setStateAtHeight(height, blockStateNew) + delete(sc.pendingTime, height) + delete(sc.pendingBlocks, height) + } + } + + for height, rcvPeerID := range sc.receivedBlocks { + if rcvPeerID == peerID { + sc.setStateAtHeight(height, blockStateNew) + delete(sc.receivedBlocks, height) + } + } + + peer.state = peerStateRemoved + + return nil +} + +func (sc *schedule) setPeerHeight(peerID p2p.ID, height int64) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Can't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Cannot set peer height for a peer in peerStateRemoved") + } + + if height < peer.height { + return fmt.Errorf("Cannot move peer height lower. from %d to %d", peer.height, height) + } + + peer.height = height + peer.state = peerStateReady + for i := sc.minHeight(); i <= height; i++ { + if sc.getStateAtHeight(i) == blockStateUnknown { + sc.setStateAtHeight(i, blockStateNew) + } + } + + return nil +} + +func (sc *schedule) getStateAtHeight(height int64) blockState { + if height < sc.initHeight { + return blockStateProcessed + } else if state, ok := sc.blockStates[height]; ok { + return state + } else { + return blockStateUnknown + } +} + +func (sc *schedule) getPeersAtHeight(height int64) []*scPeer { + peers := []*scPeer{} + for _, peer := range sc.peers { + if peer.height >= height { + peers = append(peers, peer) + } + } + + return peers +} + +func (sc *schedule) peersInactiveSince(duration time.Duration, now time.Time) []p2p.ID { + peers := []p2p.ID{} + for _, peer := range sc.peers { + if now.Sub(peer.lastTouched) > duration { + peers = append(peers, peer.peerID) + } + } + + return peers +} + +func (sc *schedule) peersSlowerThan(minSpeed int64) []p2p.ID { + peers := []p2p.ID{} + for _, peer := range sc.peers { + if peer.lastRate < minSpeed { + peers = append(peers, peer.peerID) + } + } + + return peers +} + +func (sc *schedule) setStateAtHeight(height int64, state blockState) { + sc.blockStates[height] = state +} + +func (sc *schedule) markReceived(peerID p2p.ID, height int64, size int64, now time.Time) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Can't find peer %s", peerID) + } + + if peer.state == peerStateRemoved { + return fmt.Errorf("Cannot receive blocks from removed peer %s", peerID) + } + + if state := sc.getStateAtHeight(height); state != blockStatePending || sc.pendingBlocks[height] != peerID { + return fmt.Errorf("Received block %d from peer %s without being requested", height, peerID) + } + + pendingTime, ok := sc.pendingTime[height] + if !ok || now.Sub(pendingTime) <= 0 { + return fmt.Errorf("Clock error. Block %d received at %s but requested at %s", + height, pendingTime, now) + } + + peer.lastRate = size / int64(now.Sub(pendingTime).Seconds()) + + sc.setStateAtHeight(height, blockStateReceived) + delete(sc.pendingBlocks, height) + delete(sc.pendingTime, height) + + sc.receivedBlocks[height] = peerID + + return nil +} + +func (sc *schedule) markPending(peerID p2p.ID, height int64, time time.Time) error { + peer, ok := sc.peers[peerID] + if !ok { + return fmt.Errorf("Can't find peer %s", peerID) + } + + state := sc.getStateAtHeight(height) + if state != blockStateNew { + return fmt.Errorf("Block %d should be in blockStateNew but was %s", height, state) + } + + if peer.state != peerStateReady { + return fmt.Errorf("Cannot schedule %d from %s in %s", height, peerID, peer.state) + } + + if height > peer.height { + return fmt.Errorf("Cannot request height %d from peer %s who is at height %d", + height, peerID, peer.height) + } + + sc.setStateAtHeight(height, blockStatePending) + sc.pendingBlocks[height] = peerID + // XXX: to make this more accurate we can introduce a message from + // the IO routine which indicates the time the request was put on the wire + sc.pendingTime[height] = time + + return nil +} + +func (sc *schedule) markProcessed(height int64) error { + state := sc.getStateAtHeight(height) + if state != blockStateReceived { + return fmt.Errorf("Can't mark height %d received from block state %s", height, state) + } + + delete(sc.receivedBlocks, height) + + sc.setStateAtHeight(height, blockStateProcessed) + + return nil +} + +// allBlockProcessed returns true if all blocks are in blockStateProcessed and +// determines if the schedule has been completed +func (sc *schedule) allBlocksProcessed() bool { + for _, state := range sc.blockStates { + if state != blockStateProcessed { + return false + } + } + return true +} + +// highest block | state == blockStateNew +func (sc *schedule) maxHeight() int64 { + var max int64 = 0 + for height, state := range sc.blockStates { + if state == blockStateNew && height > max { + max = height + } + } + + return max +} + +// lowest block | state == blockStateNew +func (sc *schedule) minHeight() int64 { + var min int64 = math.MaxInt64 + for height, state := range sc.blockStates { + if state == blockStateNew && height < min { + min = height + } + } + + return min +} + +func (sc *schedule) pendingFrom(peerID p2p.ID) []int64 { + heights := []int64{} + for height, pendingPeerID := range sc.pendingBlocks { + if pendingPeerID == peerID { + heights = append(heights, height) + } + } + return heights +} + +func (sc *schedule) selectPeer(peers []*scPeer) *scPeer { + // FIXME: properPeerSelector + s := rand.NewSource(time.Now().Unix()) + r := rand.New(s) + + return peers[r.Intn(len(peers))] +} + +// XXX: this duplicates the logic of peersInactiveSince and peersSlowerThan +func (sc *schedule) prunablePeers(peerTimout time.Duration, minRecvRate int64, now time.Time) []p2p.ID { + prunable := []p2p.ID{} + for peerID, peer := range sc.peers { + if now.Sub(peer.lastTouched) > peerTimout || peer.lastRate < minRecvRate { + prunable = append(prunable, peerID) + } + } + + return prunable +} + +func (sc *schedule) numBlockInState(targetState blockState) uint32 { + var num uint32 = 0 + for _, state := range sc.blockStates { + if state == targetState { + num++ + } + } + return num +} diff --git a/blockchain/v2/schedule_test.go b/blockchain/v2/schedule_test.go new file mode 100644 index 000000000..a1448c528 --- /dev/null +++ b/blockchain/v2/schedule_test.go @@ -0,0 +1,272 @@ +package v2 + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/tendermint/tendermint/p2p" +) + +func TestScheduleInit(t *testing.T) { + var ( + initHeight int64 = 5 + sc = newSchedule(initHeight) + ) + + assert.Equal(t, blockStateNew, sc.getStateAtHeight(initHeight)) + assert.Equal(t, blockStateProcessed, sc.getStateAtHeight(initHeight-1)) + assert.Equal(t, blockStateUnknown, sc.getStateAtHeight(initHeight+1)) +} + +func TestAddPeer(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerIDTwo p2p.ID = "2" + sc = newSchedule(initHeight) + ) + + assert.Nil(t, sc.addPeer(peerID)) + assert.Nil(t, sc.addPeer(peerIDTwo)) + assert.Error(t, sc.addPeer(peerID)) +} + +func TestTouchPeer(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + sc = newSchedule(initHeight) + now = time.Now() + ) + + assert.Error(t, sc.touchPeer(peerID, now), + "Touching an unknown peer should return errPeerNotFound") + + assert.Nil(t, sc.addPeer(peerID), + "Adding a peer should return no error") + assert.Nil(t, sc.touchPeer(peerID, now), + "Touching a peer should return no error") + + threshold := 10 * time.Second + assert.Empty(t, sc.peersInactiveSince(threshold, now.Add(9*time.Second)), + "Expected no peers to have been touched over 9 seconds") + assert.Containsf(t, sc.peersInactiveSince(threshold, now.Add(11*time.Second)), peerID, + "Expected one %s to have been touched over 10 seconds ago", peerID) +} + +func TestPeerHeight(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + sc = newSchedule(initHeight) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight)) + for i := initHeight; i <= peerHeight; i++ { + assert.Equal(t, sc.getStateAtHeight(i), blockStateNew, + "Expected all blocks to be in blockStateNew") + peerIDs := []p2p.ID{} + for _, peer := range sc.getPeersAtHeight(i) { + peerIDs = append(peerIDs, peer.peerID) + } + + assert.Containsf(t, peerIDs, peerID, + "Expected %s to have block %d", peerID, i) + } +} + +func TestTransitionPending(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerIDTwo p2p.ID = "2" + peerHeight int64 = 20 + sc = newSchedule(initHeight) + now = time.Now() + ) + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + assert.Nil(t, sc.addPeer(peerIDTwo), + "Adding a peer should return no error") + + assert.Error(t, sc.markPending(peerID, peerHeight, now), + "Expected scheduling a block from a peer in peerStateNew to fail") + + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + assert.NoError(t, sc.setPeerHeight(peerIDTwo, peerHeight), + "Expected setPeerHeight to return no error") + + assert.NoError(t, sc.markPending(peerID, peerHeight, now), + "Expected markingPending new block to succeed") + assert.Error(t, sc.markPending(peerIDTwo, peerHeight, now), + "Expected markingPending by a second peer to fail") + + assert.Equal(t, blockStatePending, sc.getStateAtHeight(peerHeight), + "Expected the block to to be in blockStatePending") + + assert.NoError(t, sc.removePeer(peerID), + "Expected removePeer to return no error") + + assert.Equal(t, blockStateNew, sc.getStateAtHeight(peerHeight), + "Expected the block to to be in blockStateNew") + + assert.Error(t, sc.markPending(peerID, peerHeight, now), + "Expected markingPending removed peer to fail") + + assert.NoError(t, sc.markPending(peerIDTwo, peerHeight, now), + "Expected markingPending on a ready peer to succeed") + + assert.Equal(t, blockStatePending, sc.getStateAtHeight(peerHeight), + "Expected the block to to be in blockStatePending") +} + +func TestTransitionReceived(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerIDTwo p2p.ID = "2" + peerHeight int64 = 20 + blockSize int64 = 1024 + sc = newSchedule(initHeight) + now = time.Now() + receivedAt = now.Add(1 * time.Second) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Expected adding peer %s to succeed", peerID) + assert.NoError(t, sc.addPeer(peerIDTwo), + "Expected adding peer %s to succeed", peerIDTwo) + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + assert.NoErrorf(t, sc.setPeerHeight(peerIDTwo, peerHeight), + "Expected setPeerHeight on %s to %d to succeed", peerIDTwo, peerHeight) + assert.NoError(t, sc.markPending(peerID, initHeight, now), + "Expected markingPending new block to succeed") + + assert.Error(t, sc.markReceived(peerIDTwo, initHeight, blockSize, receivedAt), + "Expected marking markReceived from a non requesting peer to fail") + + assert.NoError(t, sc.markReceived(peerID, initHeight, blockSize, receivedAt), + "Expected marking markReceived on a pending block to succeed") + + assert.Error(t, sc.markReceived(peerID, initHeight, blockSize, receivedAt), + "Expected marking markReceived on received block to fail") + + assert.Equalf(t, blockStateReceived, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockHeightReceived", initHeight) + + assert.NoErrorf(t, sc.removePeer(peerID), + "Expected removePeer removing %s to succeed", peerID) + + assert.Equalf(t, blockStateNew, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateNew", initHeight) + + assert.NoErrorf(t, sc.markPending(peerIDTwo, initHeight, now), + "Expected markingPending %d from %s to succeed", initHeight, peerIDTwo) + assert.NoErrorf(t, sc.markReceived(peerIDTwo, initHeight, blockSize, receivedAt), + "Expected marking markReceived %d from %s to succeed", initHeight, peerIDTwo) + assert.Equalf(t, blockStateReceived, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateReceived", initHeight) +} + +func TestTransitionProcessed(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + blockSize int64 = 1024 + sc = newSchedule(initHeight) + now = time.Now() + receivedAt = now.Add(1 * time.Second) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Expected adding peer %s to succeed", peerID) + assert.NoErrorf(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight on %s to %d to succeed", peerID, peerHeight) + assert.NoError(t, sc.markPending(peerID, initHeight, now), + "Expected markingPending new block to succeed") + assert.NoError(t, sc.markReceived(peerID, initHeight, blockSize, receivedAt), + "Expected marking markReceived on a pending block to succeed") + + assert.Error(t, sc.markProcessed(initHeight+1), + "Expected marking %d as processed to fail", initHeight+1) + assert.NoError(t, sc.markProcessed(initHeight), + "Expected marking %d as processed to succeed", initHeight) + + assert.Equalf(t, blockStateProcessed, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateProcessed", initHeight) + + assert.NoError(t, sc.removePeer(peerID), + "Expected removing peer %s to succeed", peerID) + + assert.Equalf(t, blockStateProcessed, sc.getStateAtHeight(initHeight), + "Expected block %d to be blockStateProcessed", initHeight) +} + +func TestMinMaxHeight(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + sc = newSchedule(initHeight) + now = time.Now() + ) + + assert.Equal(t, initHeight, sc.minHeight(), + "Expected min height to be the initialized height") + + assert.Equal(t, initHeight, sc.maxHeight(), + "Expected max height to be the initialized height") + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + + assert.Equal(t, peerHeight, sc.maxHeight(), + "Expected max height to increase to peerHeight") + + assert.Nil(t, sc.markPending(peerID, initHeight, now.Add(1*time.Second)), + "Expected marking initHeight as pending to return no error") + + assert.Equal(t, initHeight+1, sc.minHeight(), + "Expected marking initHeight as pending to move minHeight forward") +} + +func TestPeersSlowerThan(t *testing.T) { + var ( + initHeight int64 = 5 + peerID p2p.ID = "1" + peerHeight int64 = 20 + blockSize int64 = 1024 + sc = newSchedule(initHeight) + now = time.Now() + receivedAt = now.Add(1 * time.Second) + ) + + assert.NoError(t, sc.addPeer(peerID), + "Adding a peer should return no error") + + assert.NoError(t, sc.setPeerHeight(peerID, peerHeight), + "Expected setPeerHeight to return no error") + + assert.NoError(t, sc.markPending(peerID, peerHeight, now), + "Expected markingPending on to return no error") + + assert.NoError(t, sc.markReceived(peerID, peerHeight, blockSize, receivedAt), + "Expected markingPending on to return no error") + + assert.Empty(t, sc.peersSlowerThan(blockSize-1), + "expected no peers to be slower than blockSize-1 bytes/sec") + + assert.Containsf(t, sc.peersSlowerThan(blockSize+1), peerID, + "expected %s to be slower than blockSize+1 bytes/sec", peerID) +} From 6f4256c5aec05808e92b30adfdf61b0f6fe684f2 Mon Sep 17 00:00:00 2001 From: Jun Kimura Date: Tue, 6 Aug 2019 01:01:30 +0900 Subject: [PATCH 170/211] mempool: make `max_tx_bytes` configurable instead of `max_msg_bytes` (#3877) Fix #3868 (comment) Commits: * mempool: make `max_tx_bytes` configurable instead of `max_msg_bytes` * update CHANGELOG_PENDING * apply suggestions from code review --- CHANGELOG_PENDING.md | 1 + config/config.go | 9 ++++----- config/toml.go | 5 +++-- docs/tendermint-core/configuration.md | 5 +++-- mempool/clist_mempool.go | 4 ++-- mempool/clist_mempool_test.go | 4 ++-- mempool/reactor.go | 11 ++++++----- 7 files changed, 21 insertions(+), 18 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 4b53a3941..1f745e3c7 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -15,6 +15,7 @@ program](https://hackerone.com/tendermint). - [privval] \#3370 Refactors and simplifies validator/kms connection handling. Please refer to thttps://github.com/tendermint/tendermint/pull/3370#issue-257360971 - [consensus] \#3839 Reduce "Error attempting to add vote" message severity (Error -> Info) +- [mempool] \#3877 Make `max_tx_bytes` configurable instead of `max_msg_bytes` ### BUG FIXES: diff --git a/config/config.go b/config/config.go index 60cbce1fa..18ddbdb17 100644 --- a/config/config.go +++ b/config/config.go @@ -799,8 +799,7 @@ type MempoolConfig struct { CacheSize int `mapstructure:"cache_size"` OnlyToPersistent bool `mapstructure:"only_to_persistent"` SkipTxFromPersistent bool `mapstructure:"skip_tx_from_persistent"` - MaxMsgBytes int `mapstructure:"max_msg_bytes"` - + MaxTxBytes int `mapstructure:"max_tx_bytes"` } // DefaultMempoolConfig returns a default configuration for the Tendermint mempool @@ -814,7 +813,7 @@ func DefaultMempoolConfig() *MempoolConfig { Size: 5000, MaxTxsBytes: 1024 * 1024 * 1024, // 1GB CacheSize: 10000, - MaxMsgBytes: 1024 * 1024, // 1MB + MaxTxBytes: 1024 * 1024, // 1MB OnlyToPersistent: false, SkipTxFromPersistent: false, } @@ -849,8 +848,8 @@ func (cfg *MempoolConfig) ValidateBasic() error { if cfg.CacheSize < 0 { return errors.New("cache_size can't be negative") } - if cfg.MaxMsgBytes < 0 { - return errors.New("max_msg_bytes can't be negative") + if cfg.MaxTxBytes < 0 { + return errors.New("max_tx_bytes can't be negative") } return nil } diff --git a/config/toml.go b/config/toml.go index 778962538..65db774dd 100644 --- a/config/toml.go +++ b/config/toml.go @@ -371,8 +371,9 @@ max_txs_bytes = {{ .Mempool.MaxTxsBytes }} # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = {{ .Mempool.CacheSize }} -# Limit the size of TxMessage -max_msg_bytes = {{ .Mempool.MaxMsgBytes }} +# Maximum size of a single transaction. +# NOTE: the max size of a tx transmitted over the network is {max_tx_bytes} + {amino overhead}. +max_tx_bytes = {{ .Mempool.MaxTxBytes }} ##### fast sync configuration options ##### [fastsync] diff --git a/docs/tendermint-core/configuration.md b/docs/tendermint-core/configuration.md index 82fcfd61b..3fe985d89 100644 --- a/docs/tendermint-core/configuration.md +++ b/docs/tendermint-core/configuration.md @@ -240,8 +240,9 @@ max_txs_bytes = 1073741824 # Size of the cache (used to filter transactions we saw earlier) in transactions cache_size = 10000 -# Limit the size of TxMessage -max_msg_bytes = 1048576 +# Maximum size of a single transaction. +# NOTE: the max size of a tx transmitted over the network is {max_tx_bytes} + {amino overhead}. +max_tx_bytes = 1048576 ##### fast sync configuration options ##### [fastsync] diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 18cbccd47..109064360 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -232,8 +232,8 @@ func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), t // The size of the corresponding amino-encoded TxMessage // can't be larger than the maxMsgSize, otherwise we can't // relay it to peers. - if max := calcMaxTxSize(mem.config.MaxMsgBytes); txSize > max { - return ErrTxTooLarge{max, txSize} + if txSize > mem.config.MaxTxBytes { + return ErrTxTooLarge{mem.config.MaxTxBytes, txSize} } if mem.preCheck != nil { diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index c8d5a93de..3c51752b9 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -528,8 +528,8 @@ func TestMempoolMaxMsgSize(t *testing.T) { mempl, cleanup := newMempoolWithApp(cc) defer cleanup() - maxMsgSize := mempl.config.MaxMsgBytes - maxTxSize := calcMaxTxSize(mempl.config.MaxMsgBytes) + maxTxSize := mempl.config.MaxTxBytes + maxMsgSize := calcMaxMsgSize(maxTxSize) testCases := []struct { len int diff --git a/mempool/reactor.go b/mempool/reactor.go index 14830a38b..adbc4fab9 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -292,8 +292,9 @@ func RegisterMempoolMessages(cdc *amino.Codec) { } func (memR *Reactor) decodeMsg(bz []byte) (msg MempoolMessage, err error) { - if l := len(bz); l > memR.config.MaxMsgBytes { - return msg, ErrTxTooLarge{memR.config.MaxMsgBytes, l} + maxMsgSize := calcMaxMsgSize(memR.config.MaxTxBytes) + if l := len(bz); l > maxMsgSize { + return msg, ErrTxTooLarge{maxMsgSize, l} } err = cdc.UnmarshalBinaryBare(bz, &msg) return @@ -311,8 +312,8 @@ func (m *TxMessage) String() string { return fmt.Sprintf("[TxMessage %v]", m.Tx) } -// calcMaxTxSize returns the max size of Tx +// calcMaxMsgSize returns the max size of TxMessage // account for amino overhead of TxMessage -func calcMaxTxSize(maxMsgSize int) int { - return maxMsgSize - aminoOverheadForTxMessage +func calcMaxMsgSize(maxTxSize int) int { + return maxTxSize + aminoOverheadForTxMessage } From 49eca03e6611fe55296ef7b654ed9e47c18787b7 Mon Sep 17 00:00:00 2001 From: Marko Date: Tue, 6 Aug 2019 10:25:24 +0200 Subject: [PATCH 171/211] [ADR - 42] State-sync (#3769) * init adr 042: state sync * link to blockchain reactor * brapse proposal * formatting * response to feedback * Update docs/architecture/adr-042-state-sync.md Co-Authored-By: Aditya * Update security models and more * clarify compression * typo * Update docs/architecture/adr-042-state-sync.md Co-Authored-By: Anton Kaliaev --- docs/architecture/adr-042-state-sync.md | 239 ++++++++++++++++++++++++ docs/architecture/img/state-sync.png | Bin 0 -> 101084 bytes 2 files changed, 239 insertions(+) create mode 100644 docs/architecture/adr-042-state-sync.md create mode 100644 docs/architecture/img/state-sync.png diff --git a/docs/architecture/adr-042-state-sync.md b/docs/architecture/adr-042-state-sync.md new file mode 100644 index 000000000..d525a4974 --- /dev/null +++ b/docs/architecture/adr-042-state-sync.md @@ -0,0 +1,239 @@ +# ADR 042: State Sync Design + +## Changelog + +2019-06-27: Init by EB +2019-07-04: Follow up by brapse + +## Context +StateSync is a feature which would allow a new node to receive a +snapshot of the application state without downloading blocks or going +through consensus. Once downloaded, the node could switch to FastSync +and eventually participate in consensus. The goal of StateSync is to +facilitate setting up a new node as quickly as possible. + +## Considerations +Because Tendermint doesn't know anything about the application state, +StateSync will broker messages between nodes and through +the ABCI to an opaque applicaton. The implementation will have multiple +touch points on both the tendermint code base and ABCI application. + +* A StateSync reactor to facilitate peer communication - Tendermint +* A Set of ABCI messages to transmit application state to the reactor - Tendermint +* A Set of MultiStore APIs for exposing snapshot data to the ABCI - ABCI application +* A Storage format with validation and performance considerations - ABCI application + +### Implementation Properties +Beyond the approach, any implementation of StateSync can be evaluated +across different criteria: + +* Speed: Expected throughput of producing and consuming snapshots +* Safety: Cost of pushing invalid snapshots to a node +* Liveness: Cost of preventing a node from receiving/constructing a snapshot +* Effort: How much effort does an implementation require + +### Implementation Question +* What is the format of a snapshot + * Complete snapshot + * Ordered IAVL key ranges + * Compressed individually chunks which can be validated +* How is data validated + * Trust a peer with it's data blindly + * Trust a majority of peers + * Use light client validation to validate each chunk against consensus + produced merkle tree root +* What are the performance characteristics + * Random vs sequential reads + * How parallelizeable is the scheduling algorithm + +### Proposals +Broadly speaking there are two approaches to this problem which have had +varying degrees of discussion and progress. These approach can be +summarized as: + +**Lazy:** Where snapshots are produced dynamically at request time. This +solution would use the existing data structure. +**Eager:** Where snapshots are produced periodically and served from disk at +request time. This solution would create an auxiliary data structure +optimized for batch read/writes. + +Additionally the propsosals tend to vary on how they provide safety +properties. + +**LightClient** Where a client can aquire the merkle root from the block +headers synchronized from a trusted validator set. Subsets of the application state, +called chunks can therefore be validated on receipt to ensure each chunk +is part of the merkle root. + +**Majority of Peers** Where manifests of chunks along with checksums are +downloaded and compared against versions provided by a majority of +peers. + +#### Lazy StateSync +An [initial specification](https://docs.google.com/document/d/15MFsQtNA0MGBv7F096FFWRDzQ1vR6_dics5Y49vF8JU/edit?ts=5a0f3629) was published by Alexis Sellier. +In this design, the state has a given `size` of primitive elements (like +keys or nodes), each element is assigned a number from 0 to `size-1`, +and chunks consists of a range of such elements. Ackratos raised +[some concerns](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/edit) +about this design, somewhat specific to the IAVL tree, and mainly concerning +performance of random reads and of iterating through the tree to determine element numbers +(ie. elements aren't indexed by the element number). + +An alternative design was suggested by Jae Kwon in +[#3639](https://github.com/tendermint/tendermint/issues/3639) where chunking +happens lazily and in a dynamic way: nodes request key ranges from their peers, +and peers respond with some subset of the +requested range and with notes on how to request the rest in parallel from other +peers. Unlike chunk numbers, keys can be verified directly. And if some keys in the +range are ommitted, proofs for the range will fail to verify. +This way a node can start by requesting the entire tree from one peer, +and that peer can respond with say the first few keys, and the ranges to request +from other peers. + +Additionally, per chunk validation tends to come more naturally to the +Lazy approach since it tends to use the existing structure of the tree +(ie. keys or nodes) rather than state-sync specific chunks. Such a +design for tendermint was originally tracked in +[#828](https://github.com/tendermint/tendermint/issues/828). + +#### Eager StateSync +Warp Sync as implemented in Parity +["Warp Sync"](https://wiki.parity.io/Warp-Sync-Snapshot-Format.html) to rapidly +download both blocks and state snapshots from peers. Data is carved into ~4MB +chunks and snappy compressed. Hashes of snappy compressed chunks are stored in a +manifest file which co-ordinates the state-sync. Obtaining a correct manifest +file seems to require an honest majority of peers. This means you may not find +out the state is incorrect until you download the whole thing and compare it +with a verified block header. + +A similar solution was implemented by Binance in +[#3594](https://github.com/tendermint/tendermint/pull/3594) +based on their initial implementation in +[PR #3243](https://github.com/tendermint/tendermint/pull/3243) +and [some learnings](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/edit). +Note this still requires the honest majority peer assumption. + +As an eager protocol, warp-sync can efficiently compress larger, more +predicatable chunks once per snapshot and service many new peers. By +comparison lazy chunkers would have to compress each chunk at request +time. + +### Analysis of Lazy vs Eager +Lazy vs Eager have more in common than they differ. They all require +reactors on the tendermint side, a set of ABCI messages and a method for +serializing/deserializing snapshots facilitated by a SnapshotFormat. + +The biggest difference between Lazy and Eager proposals is in the +read/write patterns necessitated by serving a snapshot chunk. +Specifically, Lazy State Sync performs random reads to the underlying data +structure while Eager can optimize for sequential reads. + +This distinctin between approaches was demonstrated by Binance's +[ackratos](https://github.com/ackratos) in their implementation of [Lazy +State sync](https://github.com/tendermint/tendermint/pull/3243), The +[analysis](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/) +of the performance, and follow up implementation of [Warp +Sync](http://github.com/tendermint/tendermint/pull/3594). + +#### Compairing Security Models +There are several different security models which have been +discussed/proposed in the past but generally fall into two categories. + +Light client validation: In which the node receiving data is expected to +first perform a light client sync and have all the nessesary block +headers. Within the trusted block header (trusted in terms of from a +validator set subject to [weak +subjectivity](https://github.com/tendermint/tendermint/pull/3795)) and +can compare any subset of keys called a chunk against the merkle root. +The advantage of light client validation is that the block headers are +signed by validators which have something to lose for malicious +behaviour. If a validator were to provide an invalid proof, they can be +slashed. + +Majority of peer validation: A manifest file containing a list of chunks +along with checksums of each chunk is downloaded from a +trusted source. That source can be a community resource similar to +[sum.golang.org](https://sum.golang.org) or downloaded from the majority +of peers. One disadantage of the majority of peer security model is the +vuliberability to eclipse attacks in which a malicious users looks to +saturate a target node's peer list and produce a manufactured picture of +majority. + +A third option would be to include snapshot related data in the +block header. This could include the manifest with related checksums and be +secured through consensus. One challenge of this approach is to +ensure that creating snapshots does not put undo burden on block +propsers by synchronizing snapshot creation and block creation. One +approach to minimizing the burden is for snapshots for height +`H` to be included in block `H+n` where `n` is some `n` block away, +giving the block propser enough time to complete the snapshot +asynchronousy. + +## Proposal: Eager StateSync With Per Chunk Light Client Validation +The conclusion after some concideration of the advantages/disadvances of +eager/lazy and different security models is to produce a state sync +which eagerly produces snapshots and uses light client validation. This +approach has the performance advantages of pre-computing efficient +snapshots which can streamed to new nodes on demand using sequential IO. +Secondly, by using light client validation we cna validate each chunk on +receipt and avoid the potential eclipse attack of majority of peer based +security. + +### Implementation +Tendermint is responsible for downloading and verifying chunks of +AppState from peers. ABCI Application is responsible for taking +AppStateChunk objects from TM and constructing a valid state tree whose +root corresponds with the AppHash of syncing block. In particular we +will need implement: + +* Build new StateSync reactor brokers message transmission between the peers + and the ABCI application +* A set of ABCI Messages +* Design SnapshotFormat as an interface which can: + * validate chunks + * read/write chunks from file + * read/write chunks to/from application state store + * convert manifests into chunkRequest ABCI messages +* Implement SnapshotFormat for cosmos-hub with concrete implementation for: + * read/write chunks in a way which can be: + * parallelized across peers + * validated on receipt + * read/write to/from IAVL+ tree + +![StateSync Architecture Diagram](img/state-sync.png) + +## Implementation Path +* Create StateSync reactor based on [#3753](https://github.com/tendermint/tendermint/pull/3753) +* Design SnapshotFormat with an eye towards cosmos-hub implementation +* ABCI message to send/receive SnapshotFormat +* IAVL+ changes to support SnapshotFormat +* Deliver Warp sync (no chunk validation) +* light client implementation for weak subjectivity +* Deliver StateSync with chunk validation + +## Status + +Proposed + +## Concequences + +### Neutral + +### Positive +* Safe & performant state sync design substantiated with real world implementation experience +* General interfaces allowing application specific innovation +* Parallizable implementation trajectory with reasonable engineering effort + +### Negative +* Static Scheduling lacks opportunity for real time chunk availability optimizations + +## References +[sync: Sync current state without full replay for Applications](https://github.com/tendermint/tendermint/issues/828) - original issue +[tendermint state sync proposal](https://docs.google.com/document/d/15MFsQtNA0MGBv7F096FFWRDzQ1vR6_dics5Y49vF8JU/edit?ts=5a0f3629) - Cloudhead proposal +[tendermint state sync proposal 2](https://docs.google.com/document/d/1npGTAa1qxe8EQZ1wG0a0Sip9t5oX2vYZNUDwr_LVRR4/edit) - ackratos proposal +[proposal 2 implementation](https://github.com/tendermint/tendermint/pull/3243) - ackratos implementation +[WIP General/Lazy State-Sync pseudo-spec](https://github.com/tendermint/tendermint/issues/3639) - Jae Proposal +[Warp Sync Implementation](https://github.com/tendermint/tendermint/pull/3594) - ackratos +[Chunk Proposal](https://github.com/tendermint/tendermint/pull/3799) - Bucky proposed + + diff --git a/docs/architecture/img/state-sync.png b/docs/architecture/img/state-sync.png new file mode 100644 index 0000000000000000000000000000000000000000..08b6eac43f4af592ca3c1fa9f9003ac0c04f5b12 GIT binary patch literal 101084 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

+ + + diff --git a/docs/spec/rpc/swagger.yaml b/docs/spec/rpc/swagger.yaml new file mode 100644 index 000000000..70fa82014 --- /dev/null +++ b/docs/spec/rpc/swagger.yaml @@ -0,0 +1,2680 @@ +swagger: "2.0" +info: + version: "0.32.1" + title: RPC client for Tendermint + description: A REST interface for state queries, transaction generation and broadcasting. + license: + name: Apache 2.0 + url: https://github.com/tendermint/tendermint/blob/master/LICENSE +tags: + - name: Websocket + description: Subscribe/unsubscribe are reserved for websocket events. + - name: Info + description: Informations about the node APIs + - name: Tx + description: Transactions broadcast APIs + - name: ABCI + description: ABCI APIs + - name: Evidence + description: Evidence APIs +schemes: + - https +host: stargate.cosmos.network:26657 +securityDefinitions: + kms: + type: basic +paths: + /broadcast_tx_sync: + get: + summary: Returns with the response from CheckTx. Does not wait for DeliverTx result. + tags: + - Tx + operationId: broadcast_tx_sync + description: | + If you want to be sure that the transaction is included in a block, you can + subscribe for the result using JSONRPC via a websocket. See + https://tendermint.com/docs/app-dev/subscribing-to-events-via-websocket.html + If you haven't received anything after a couple of blocks, resend it. If the + same happens again, send it to some other node. A few reasons why it could + happen: + + 1. malicious node can drop or pretend it had committed your tx + 2. malicious proposer (not necessary the one you're communicating with) can + drop transactions, which might become valid in the future + (https://github.com/tendermint/tendermint/issues/3322) + + + Please refer to + https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting + for formatting/encoding rules. + parameters: + - in: query + name: tx + type: string + required: true + description: The transaction + x-example: "456" + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/BroadcastTxResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /broadcast_tx_async: + get: + summary: Returns right away, with no response. Does not wait for CheckTx nor DeliverTx results. + tags: + - Tx + operationId: broadcast_tx_async + description: | + If you want to be sure that the transaction is included in a block, you can + subscribe for the result using JSONRPC via a websocket. See + https://tendermint.com/docs/app-dev/subscribing-to-events-via-websocket.html + If you haven't received anything after a couple of blocks, resend it. If the + same happens again, send it to some other node. A few reasons why it could + happen: + + 1. malicious node can drop or pretend it had committed your tx + 2. malicious proposer (not necessary the one you're communicating with) can + drop transactions, which might become valid in the future + (https://github.com/tendermint/tendermint/issues/3322) + 3. node can be offline + + Please refer to + https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting + for formatting/encoding rules. + parameters: + - in: query + name: tx + type: string + required: true + description: The transaction + x-example: "123" + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/BroadcastTxResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /broadcast_tx_commit: + get: + summary: Returns with the responses from CheckTx and DeliverTx. + tags: + - Tx + operationId: broadcast_tx_commit + description: | + IMPORTANT: use only for testing and development. In production, use + BroadcastTxSync or BroadcastTxAsync. You can subscribe for the transaction + result using JSONRPC via a websocket. See + https://tendermint.com/docs/app-dev/subscribing-to-events-via-websocket.html + + CONTRACT: only returns error if mempool.CheckTx() errs or if we timeout + waiting for tx to commit. + + If CheckTx or DeliverTx fail, no error will be returned, but the returned result + will contain a non-OK ABCI code. + + Please refer to + https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting + for formatting/encoding rules. + parameters: + - in: query + name: tx + type: string + required: true + description: The transaction + x-example: "785" + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/BroadcastTxCommitResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /subscribe: + get: + summary: Subscribe for events via WebSocket. + tags: + - Websocket + operationId: subscribe + description: | + To tell which events you want, you need to provide a query. query is a + string, which has a form: "condition AND condition ..." (no OR at the + moment). condition has a form: "key operation operand". key is a string with + a restricted set of possible symbols ( \t\n\r\\()"'=>< are not allowed). + operation can be "=", "<", "<=", ">", ">=", "CONTAINS". operand can be a + string (escaped with single quotes), number, date or time. + + Examples: + tm.event = 'NewBlock' # new blocks + tm.event = 'CompleteProposal' # node got a complete proposal + tm.event = 'Tx' AND tx.hash = 'XYZ' # single transaction + tm.event = 'Tx' AND tx.height = 5 # all txs of the fifth block + tx.height = 5 # all txs of the fifth block + + Tendermint provides a few predefined keys: tm.event, tx.hash and tx.height. + Note for transactions, you can define additional keys by providing events with + DeliverTx response. + + import ( + abci "github.com/tendermint/tendermint/abci/types" + "github.com/tendermint/tendermint/libs/pubsub/query" + ) + + abci.ResponseDeliverTx{ + Events: []abci.Event{ + { + Type: "rewards.withdraw", + Attributes: cmn.KVPairs{ + cmn.KVPair{Key: []byte("address"), Value: []byte("AddrA")}, + cmn.KVPair{Key: []byte("source"), Value: []byte("SrcX")}, + cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, + }, + }, + { + Type: "rewards.withdraw", + Attributes: cmn.KVPairs{ + cmn.KVPair{Key: []byte("address"), Value: []byte("AddrB")}, + cmn.KVPair{Key: []byte("source"), Value: []byte("SrcY")}, + cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, + cmn.KVPair{Key: []byte("balance"), Value: []byte("...")}, + }, + }, + { + Type: "transfer", + Attributes: cmn.KVPairs{ + cmn.KVPair{Key: []byte("sender"), Value: []byte("AddrC")}, + cmn.KVPair{Key: []byte("recipient"), Value: []byte("AddrD")}, + cmn.KVPair{Key: []byte("amount"), Value: []byte("...")}, + }, + }, + }, + } + + All events are indexed by a composite key of the form {eventType}.{evenAttrKey}. + In the above examples, the following keys would be indexed: + - rewards.withdraw.address + - rewards.withdraw.source + - rewards.withdraw.amount + - rewards.withdraw.balance + - transfer.sender + - transfer.recipient + - transfer.amount + + Multiple event types with duplicate keys are allowed and are meant to + categorize unique and distinct events. In the above example, all events + indexed under the key `rewards.withdraw.address` will have the following + values stored and queryable: + + - AddrA + - AddrB + + To create a query for txs where address AddrA withdrew rewards: + query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA'") + + To create a query for txs where address AddrA withdrew rewards from source Y: + query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrA' AND rewards.withdraw.source = 'Y'") + + To create a query for txs where AddrA transferred funds: + query.MustParse("tm.event = 'Tx' AND transfer.sender = 'AddrA'") + + The following queries would return no results: + query.MustParse("tm.event = 'Tx' AND transfer.sender = 'AddrZ'") + query.MustParse("tm.event = 'Tx' AND rewards.withdraw.address = 'AddrZ'") + query.MustParse("tm.event = 'Tx' AND rewards.withdraw.source = 'W'") + + See list of all possible events here + https://godoc.org/github.com/tendermint/tendermint/types#pkg-constants + + For complete query syntax, check out + https://godoc.org/github.com/tendermint/tendermint/libs/pubsub/query. + + ```go + import "github.com/tendermint/tendermint/types" + + client := client.NewHTTP("tcp:0.0.0.0:26657", "/websocket") + err := client.Start() + if err != nil { + handle error + } + defer client.Stop() + ctx, cancel := context.WithTimeout(context.Background(), 1 * time.Second) + defer cancel() + query := "tm.event = 'Tx' AND tx.height = 3" + txs, err := client.Subscribe(ctx, "test-client", query) + if err != nil { + handle error + } + + go func() { + for e := range txs { + fmt.Println("got ", e.Data.(types.EventDataTx)) + } + }() + ``` + parameters: + - in: query + name: query + type: string + required: true + description: | + query is a string, which has a form: "condition AND condition ..." (no OR at the + moment). condition has a form: "key operation operand". key is a string with + a restricted set of possible symbols ( \t\n\r\\()"'=>< are not allowed). + operation can be "=", "<", "<=", ">", ">=", "CONTAINS". operand can be a + string (escaped with single quotes), number, date or time. + x-example: tm.event = 'Tx' AND tx.height = 5 + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/EmptyResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /unsubscribe: + get: + summary: Unsubscribe from event on Websocket + tags: + - Websocket + operationId: unsubscribe + description: | + ```go + client := client.NewHTTP("tcp:0.0.0.0:26657", "/websocket") + err := client.Start() + if err != nil { + handle error + } + defer client.Stop() + query := "tm.event = 'Tx' AND tx.height = 3" + err = client.Unsubscribe(context.Background(), "test-client", query) + if err != nil { + handle error + } + ``` + parameters: + - in: query + name: query + type: string + required: true + description: | + query is a string, which has a form: "condition AND condition ..." (no OR at the + moment). condition has a form: "key operation operand". key is a string with + a restricted set of possible symbols ( \t\n\r\\()"'=>< are not allowed). + operation can be "=", "<", "<=", ">", ">=", "CONTAINS". operand can be a + string (escaped with single quotes), number, date or time. + x-example: tm.event = 'Tx' AND tx.height = 5 + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/EmptyResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /unsubscribe_all: + get: + summary: Unsubscribe from all events via WebSocket + tags: + - Websocket + operationId: unsubscribe_all + description: | + Unsubscribe from all events via WebSocket + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/EmptyResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /health: + get: + summary: Node heartbeat + tags: + - Info + operationId: health + description: | + Get node health. Returns empty result (200 OK) on success, no response - in case of an error. + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/EmptyResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /status: + get: + summary: Node Status + operationId: status + tags: + - Info + description: | + Get Tendermint status including node info, pubkey, latest block hash, app hash, block height and time. + produces: + - application/json + responses: + 200: + description: Status of the node + schema: + $ref: '#/definitions/StatusResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /net_info: + get: + summary: Network informations + operationId: net_info + tags: + - Info + description: | + Get network info. + produces: + - application/json + responses: + 200: + description: empty answer + schema: + $ref: '#/definitions/NetInfoResponse' + 500: + description: empty error + schema: + $ref: '#/definitions/ErrorResponse' + /blockchain: + get: + summary: Get block headers for minHeight <= height <= maxHeight. + operationId: blockchain + parameters: + - in: query + name: minHeight + type: number + description: Minimum block height to return + x-example: 1 + - in: query + name: maxHeight + type: number + description: Maximum block height to return + x-example: 2 + tags: + - Info + description: | + Get Blockchain info. + produces: + - application/json + responses: + 200: + description: Block headers, returned in descending order (highest first). + schema: + $ref: '#/definitions/BlockchainResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /block: + get: + summary: Get block at a specified height + operationId: block + parameters: + - in: query + name: height + type: number + description: height to return. If no height is provided, it will fetch the latest block. 0 means latest + default: 0 + x-example: 1 + tags: + - Info + description: | + Get Block. + produces: + - application/json + responses: + 200: + description: Block informations. + schema: + $ref: '#/definitions/BlockResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /block_results: + get: + summary: Get block results at a specified height + operationId: block_results + parameters: + - in: query + name: height + type: number + description: height to return. If no height is provided, it will fetch informations regarding the latest block. 0 means latest + default: 0 + x-example: 1 + tags: + - Info + description: | + Get block_results. + produces: + - application/json + responses: + 200: + description: Block results. + schema: + $ref: '#/definitions/BlockResultsResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /commit: + get: + summary: Get commit results at a specified height + operationId: commit + parameters: + - in: query + name: height + type: number + description: height to return. If no height is provided, it will fetch commit informations regarding the latest block. 0 means latest + default: 0 + x-example: 1 + tags: + - Info + description: | + Get Commit. + produces: + - application/json + responses: + 200: + description: Commit results. + schema: + $ref: '#/definitions/CommitResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /validators: + get: + summary: Get validator set at a specified height + operationId: validators + parameters: + - in: query + name: height + type: number + description: height to return. If no height is provided, it will fetch validato set at the latest block. 0 means latest + default: 0 + x-example: 1 + tags: + - Info + description: | + Get Validators. + produces: + - application/json + responses: + 200: + description: Commit results. + schema: + $ref: '#/definitions/ValidatorsResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /genesis: + get: + summary: Get Genesis + operationId: genesis + tags: + - Info + description: | + Get genesis. + produces: + - application/json + responses: + 200: + description: Genesis results. + schema: + $ref: '#/definitions/GenesisResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /dump_consensus_state: + get: + summary: Get consensus state + operationId: dump_consensus_state + tags: + - Info + description: | + Get consensus state. + produces: + - application/json + responses: + 200: + description: consensus state results. + schema: + $ref: '#/definitions/DumpConsensusResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /consensus_state: + get: + summary: Get consensus state + operationId: consensus_state + tags: + - Info + description: | + Get consensus state. + produces: + - application/json + responses: + 200: + description: consensus state results. + schema: + $ref: '#/definitions/ConsensusStateResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /consensus_params: + get: + summary: Get consensus parameters + operationId: consensus_params + parameters: + - in: query + name: height + type: number + description: height to return. If no height is provided, it will fetch commit informations regarding the latest block. 0 means latest + default: 0 + x-example: 1 + tags: + - Info + description: | + Get consensus parameters. + produces: + - application/json + responses: + 200: + description: consensus parameters results. + schema: + $ref: '#/definitions/ConsensusParamsResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /unconfirmed_txs: + get: + summary: Get the list of unconfirmed transactions + operationId: unconfirmed_txs + parameters: + - in: query + name: limit + type: number + description: Maximum number of unconfirmed transactions to return + x-example: 1 + tags: + - Info + description: | + Get list of unconfirmed transactions + produces: + - application/json + responses: + 200: + description: List of unconfirmed transactions + schema: + $ref: '#/definitions/UnconfirmedTransactionsResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /num_unconfirmed_txs: + get: + summary: Get data about unconfirmed transactions + operationId: num_unconfirmed_txs + tags: + - Info + description: | + Get data about unconfirmed transactions + produces: + - application/json + responses: + 200: + description: status about unconfirmed transactions + schema: + $ref: '#/definitions/NumUnconfirmedTransactionsResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /tx_search: + get: + summary: Search for transactions + operationId: tx_search + parameters: + - in: query + name: query + type: string + description: Query + required: true + x-example: "tx.height=1000" + - in: query + name: prove + type: boolean + description: Include proofs of the transactions inclusion in the block + required: false + x-example: true + default: false + - in: query + name: page + type: number + description: "Page number (1-based)" + required: false + x-example: 1 + default: 1 + - in: query + name: per_page + type: number + description: "Number of entries per page (max: 100)" + required: false + x-example: 30 + default: 30 + tags: + - Info + description: | + Get list of unconfirmed transactions + produces: + - application/json + responses: + 200: + description: List of unconfirmed transactions + schema: + $ref: '#/definitions/TxSearchResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /tx: + get: + summary: Get transactions by hash + operationId: tx + parameters: + - in: query + name: hash + type: string + description: transaction Hash to retrive + required: true + x-example: "0xD70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED" + - in: query + name: prove + type: boolean + description: Include proofs of the transactions inclusion in the block + required: false + x-example: true + default: false + tags: + - Info + description: | + Get a trasasction + produces: + - application/json + responses: + 200: + description: Get a transaction + schema: + $ref: '#/definitions/TxResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /abci_info: + get: + summary: Get some info about the application. + operationId: abci_info + tags: + - ABCI + description: | + Get some info about the application. + produces: + - application/json + responses: + 200: + description: Get some info about the application. + schema: + $ref: '#/definitions/ABCIInfoResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + /abci_query: + get: + summary: Query the application for some information. + operationId: abci_query + parameters: + - in: query + name: path + type: string + description: Path to the data ("/a/b/c") + required: true + x-example: "/a/b/c" + - in: query + name: data + type: string + description: Data + required: true + x-example: "IHAVENOIDEA" + - in: query + name: height + type: number + description: Height (0 means latest) + required: false + x-example: 1 + default: 0 + - in: query + name: prove + type: boolean + description: Include proofs of the transactions inclusion in the block + required: false + x-example: true + default: false + tags: + - ABCI + description: | + Query the application for some information. + produces: + - application/json + responses: + 200: + description: Response of the submitted query + schema: + $ref: '#/definitions/ABCIQueryResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + + /broadcast_evidence: + get: + summary: Broadcast evidence of the misbehavior. + operationId: broadcast_evidence + parameters: + - in: query + name: evidence + type: string + description: Amino-encoded JSON evidence + required: true + x-example: "JSON_EVIDENCE_Amino_encoded" + tags: + - Info + description: | + Broadcast evidence of the misbehavior. + produces: + - application/json + responses: + 200: + description: Broadcast evidence of the misbehavior. + schema: + $ref: '#/definitions/BroadcastEvidenceResponse' + 500: + description: Error + schema: + $ref: '#/definitions/ErrorResponse' + + +definitions: + JSONRPC: + type: object + properties: + id: + type: string + x-example: "" + jsonrpc: + type: string + x-example: "2.0" + EmptyResponse: + description: Empty Response + allOf: + - $ref: '#/definitions/JSONRPC' + - type: object + properties: + result: + type: object + additionalProperties: {} + ErrorResponse: + description: Error Response + allOf: + - $ref: '#/definitions/JSONRPC' + - type: object + properties: + error: + type: string + x-example: "Description of failure" + ProtocolVersion: + type: object + properties: + p2p: + type: string + x-example: "7" + block: + type: string + x-example: "10" + app: + type: string + x-example: "0" + PubKey: + type: object + properties: + type: + type: string + x-example: "tendermint/PubKeyEd25519" + value: + type: string + x-example: "A6DoBUypNtUAyEHWtQ9bFjfNg8Bo9CrnkUGl6k6OHN4=" + NodeInfo: + type: object + properties: + protocol_version: + $ref: '#/definitions/ProtocolVersion' + id: + type: string + x-example: "5576458aef205977e18fd50b274e9b5d9014525a" + listen_addr: + type: string + x-example: "tcp:0.0.0.0:26656" + network: + type: string + x-example: "cosmoshub-2" + version: + type: string + x-example: "0.32.1" + channels: + type: string + x-example: "4020212223303800" + moniker: + type: string + x-example: "moniker-node" + other: + type: object + properties: + tx_index: + type: string + x-example: "on" + rpc_address: + type: string + x-example: "tcp:0.0.0.0:26657" + x-example: "moniker-node" + SyncInfo: + type: object + properties: + latest_block_hash: + type: string + x-example: "790BA84C3545FCCC49A5C629CEE6EA58A6E875C3862175BDC11EE7AF54703501" + latest_app_hash: + type: string + x-example: "C9AEBB441B787D9F1D846DE51F3826F4FD386108B59B08239653ABF59455C3F8" + latest_block_height: + type: string + x-example: "1262196" + latest_block_time: + type: string + x-example: "2019-08-01T11:52:22.818762194Z" + catching_up: + type: boolean + x-example: false + ValidatorInfo: + type: object + properties: + address: + type: string + x-example: "5D6A51A8E9899C44079C6AF90618BA0369070E6E" + pub_key: + $ref: '#/definitions/PubKey' + voting_power: + type: string + x-example: "0" + Status: + description: Status Response + type: object + properties: + node_info: + $ref: '#/definitions/NodeInfo' + sync_info: + $ref: '#/definitions/SyncInfo' + validator_info: + $ref: '#/definitions/ValidatorInfo' + StatusResponse: + description: Status Response + allOf: + - $ref: '#/definitions/JSONRPC' + - type: object + properties: + result: + $ref: '#/definitions/Status' + Monitor: + type: object + properties: + Active: + type: boolean + x-example: true + Start: + type: string + x-example: "2019-07-31T14:31:28.66Z" + Duration: + type: string + x-example: "168901060000000" + Idle: + type: string + x-example: "168901040000000" + Bytes: + type: string + x-example: "5" + Samples: + type: string + x-example: "1" + InstRate: + type: string + x-example: "0" + CurRate: + type: string + x-example: "0" + AvgRate: + type: string + x-example: "0" + PeakRate: + type: string + x-example: "0" + BytesRem: + type: string + x-example: "0" + TimeRem: + type: string + x-example: "0" + Progress: + type: number + x-example: 0 + Channel: + type: object + properties: + ID: + type: number + x-example: 48 + SendQueueCapacity: + type: string + x-example: "1" + SendQueueSize: + type: string + x-example: "0" + Priority: + type: string + x-example: "5" + RecentlySent: + type: string + x-example: "0" + ConnectionStatus: + type: object + properties: + Duration: + type: string + x-example: "168901057956119" + SendMonitor: + $ref: '#/definitions/Monitor' + RecvMonitor: + $ref: '#/definitions/Monitor' + Channels: + type: array + items: + $ref: '#/definitions/Channel' + Peer: + type: object + properties: + node_info: + $ref: '#/definitions/NodeInfo' + is_outbound: + type: boolean + x-example: true + connection_status: + $ref: '#/definitions/ConnectionStatus' + remote_ip: + type: string + x-example: "95.179.155.35" + NetInfo: + type: object + properties: + listening: + type: boolean + x-example: true + listeners: + type: array + items: + type: string + x-example: "Listener(@)" + n_peers: + type: number + x-example: "1" + peers: + type: array + items: + $ref: '#/definitions/Peer' + NetInfoResponse: + description: NetInfo Response + allOf: + - $ref: '#/definitions/JSONRPC' + - type: object + properties: + result: + $ref: '#/definitions/NetInfo' + BlockID: + required: + - "hash" + - "parts" + properties: + hash: + type: string + x-example: "D82C2734BB0E76C772A10994B210EF9D11505D1B98CB189D9CF7F9A5488672A5" + parts: + required: + - "total" + - "hash" + properties: + total: + type: string + x-example: "1" + hash: + type: string + x-example: "CB02DCAA7FB46BF874052EC2273FD0B1F2CF2E1593298D9781E60FE9C3DB8638" + type: object + type: object + BlockMetaHeader: + required: + - "version" + - "chain_id" + - "height" + - "time" + - "num_txs" + - "total_txs" + - "last_block_id" + - "last_commit_hash" + - "data_hash" + - "validators_hash" + - "next_validators_hash" + - "consensus_hash" + - "app_hash" + - "last_results_hash" + - "evidence_hash" + - "proposer_address" + properties: + version: + required: + - "block" + - "app" + properties: + block: + type: string + x-example: "10" + app: + type: string + x-example: "0" + type: object + chain_id: + type: string + x-example: "cosmoshub-2" + height: + type: string + x-example: "12" + time: + type: string + x-example: "2019-04-22T17:01:51.701356223Z" + num_txs: + type: string + x-example: "2" + total_txs: + type: string + x-example: "3" + last_block_id: + $ref: '#/definitions/BlockID' + last_commit_hash: + type: string + x-example: "21B9BC845AD2CB2C4193CDD17BFC506F1EBE5A7402E84AD96E64171287A34812" + data_hash: + type: string + x-example: "970886F99E77ED0D60DA8FCE0447C2676E59F2F77302B0C4AA10E1D02F18EF73" + validators_hash: + type: string + x-example: "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0" + next_validators_hash: + type: string + x-example: "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0" + consensus_hash: + type: string + x-example: "0F2908883A105C793B74495EB7D6DF2EEA479ED7FC9349206A65CB0F9987A0B8" + app_hash: + type: string + x-example: "223BF64D4A01074DC523A80E76B9BBC786C791FB0A1893AC5B14866356FCFD6C" + last_results_hash: + type: string + x-example: "" + evidence_hash: + type: string + x-example: "" + proposer_address: + type: string + x-example: "D540AB022088612AC74B287D076DBFBC4A377A2E" + type: object + BlockMetaId: + required: + - "hash" + - "parts" + properties: + hash: + type: string + x-example: "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7" + parts: + required: + - "total" + - "hash" + properties: + total: + type: string + x-example: "1" + hash: + type: string + x-example: "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + type: object + type: object + BlockMeta: + type: object + properties: + block_id: + $ref: '#/definitions/BlockMetaId' + header: + $ref: '#/definitions/BlockMetaHeader' + Blockchain: + type: object + required: + - "last_height" + - "block_metas" + properties: + last_height: + type: string + x-example: "1276718" + block_metas: + type: "array" + items: + $ref: '#/definitions/BlockMeta' + BlockchainResponse: + description: Blockchain info + allOf: + - $ref: '#/definitions/JSONRPC' + - type: object + properties: + result: + $ref: '#/definitions/Blockchain' + Commit: + required: + - "type" + - "height" + - "round" + - "block_id" + - "timestamp" + - "validator_address" + - "validator_index" + - "signature" + properties: + type: + type: number + x-example: 2 + height: + type: string + x-example: "1262085" + round: + type: string + x-example: "0" + block_id: + $ref: '#/definitions/BlockID' + timestamp: + type: string + x-example: "2019-08-01T11:39:38.867269833Z" + validator_address: + type: string + x-example: "000001E443FD237E4B616E2FA69DF4EE3D49A94F" + validator_index: + type: string + x-example: "0" + signature: + type: string + x-example: "DBchvucTzAUEJnGYpNvMdqLhBAHG4Px8BsOBB3J3mAFCLGeuG7uJqy+nVngKzZdPhPi8RhmE/xcw/M9DOJjEDg==" + Block: + type: object + properties: + header: + $ref: '#/definitions/BlockMetaHeader' + data: + type: array + items: + type: string + x-example: 'yQHwYl3uCkKoo2GaChRnd+THLQ2RM87nEZrE19910Z28ABIUWW/t8AtIMwcyU0sT32RcMDI9GF0aEAoFdWF0b20SBzEwMDAwMDASEwoNCgV1YXRvbRIEMzEwMRCd8gEaagom61rphyEDoJPxlcjRoNDtZ9xMdvs+lRzFaHe2dl2P5R2yVCWrsHISQKkqX5H1zXAIJuC57yw0Yb03Fwy75VRip0ZBtLiYsUqkOsPUoQZAhDNP+6LY+RUwz/nVzedkF0S29NZ32QXdGv0=' + evidence: + type: array + items: + $ref: '#/definitions/Evidence' + last_commit: + type: object + properties: + block_id: + $ref: '#/definitions/BlockID' + precommits: + type: array + items: + $ref: '#/definitions/Commit' + Validator: + type: object + properties: + pub_key: + $ref: '#/definitions/PubKey' + voting_power: + type: number + address: + type: string + Evidence: + type: object + properties: + type: + type: string + height: + type: number + time: + type: number + total_voting_power: + type: number + validator: + $ref: '#/definitions/Validator' + BlockComplete: + type: object + properties: + block_meta: + $ref: '#/definitions/BlockMeta' + block: + $ref: '#/definitions/Block' + BlockResponse: + description: Blockc info + allOf: + - $ref: '#/definitions/JSONRPC' + - type: object + properties: + result: + $ref: '#/definitions/BlockComplete' + Tag: + type: object + properties: + key: + type: string + example: 'YWN0aW9u' + value: + type: string + example: 'c2VuZA==' + ################## FROM NOW ON NEEDS REFACTOR ################## + BlockResultsResponse: + type: "object" + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "height" + - "results" + properties: + height: + type: "string" + example: "12" + results: + required: + - "deliver_tx" + - "end_block" + - "begin_block" + properties: + deliver_tx: + type: "array" + x-nullable: true + items: + type: "object" + properties: + log: + type: "string" + example: "[{\"msg_index\":\"0\",\"success\":true,\"log\":\"\"}]" + gasWanted: + type: "string" + example: "25629" + gasUsed: + type: "string" + example: "25629" + tags: + type: "array" + items: + type: "object" + properties: + key: + type: "string" + example: "YWN0aW9u" + value: + type: "string" + example: "c2VuZA==" + end_block: + required: + - "validator_updates" + properties: {} + type: "object" + begin_block: + properties: {} + type: "object" + type: "object" + type: "object" + CommitResponse: + type: "object" + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "signed_header" + - "canonical" + properties: + signed_header: + required: + - "header" + - "commit" + properties: + header: + required: + - "version" + - "chain_id" + - "height" + - "time" + - "num_txs" + - "total_txs" + - "last_block_id" + - "last_commit_hash" + - "data_hash" + - "validators_hash" + - "next_validators_hash" + - "consensus_hash" + - "app_hash" + - "last_results_hash" + - "evidence_hash" + - "proposer_address" + properties: + version: + required: + - "block" + - "app" + properties: + block: + type: "string" + example: "10" + app: + type: "string" + example: "0" + type: "object" + chain_id: + type: "string" + example: "cosmoshub-2" + height: + type: "string" + example: "12" + time: + type: "string" + example: "2019-04-22T17:01:51.701356223Z" + num_txs: + type: "string" + example: "2" + total_txs: + type: "string" + example: "3" + last_block_id: + required: + - "hash" + - "parts" + properties: + hash: + type: "string" + example: "D82C2734BB0E76C772A10994B210EF9D11505D1B98CB189D9CF7F9A5488672A5" + parts: + required: + - "total" + - "hash" + properties: + total: + type: "string" + example: "1" + hash: + type: "string" + example: "CB02DCAA7FB46BF874052EC2273FD0B1F2CF2E1593298D9781E60FE9C3DB8638" + type: "object" + type: "object" + last_commit_hash: + type: "string" + example: "21B9BC845AD2CB2C4193CDD17BFC506F1EBE5A7402E84AD96E64171287A34812" + data_hash: + type: "string" + example: "970886F99E77ED0D60DA8FCE0447C2676E59F2F77302B0C4AA10E1D02F18EF73" + validators_hash: + type: "string" + example: "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0" + next_validators_hash: + type: "string" + example: "D658BFD100CA8025CFD3BECFE86194322731D387286FBD26E059115FD5F2BCA0" + consensus_hash: + type: "string" + example: "0F2908883A105C793B74495EB7D6DF2EEA479ED7FC9349206A65CB0F9987A0B8" + app_hash: + type: "string" + example: "223BF64D4A01074DC523A80E76B9BBC786C791FB0A1893AC5B14866356FCFD6C" + last_results_hash: + type: "string" + example: "" + evidence_hash: + type: "string" + example: "" + proposer_address: + type: "string" + example: "D540AB022088612AC74B287D076DBFBC4A377A2E" + type: "object" + commit: + required: + - "block_id" + - "precommits" + properties: + block_id: + required: + - "hash" + - "parts" + properties: + hash: + type: "string" + example: "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7" + parts: + required: + - "total" + - "hash" + properties: + total: + type: "string" + example: "1" + hash: + type: "string" + example: "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + type: "object" + type: "object" + precommits: + type: "array" + items: + type: "object" + properties: + type: + type: "number" + example: 2 + height: + type: "string" + example: "12" + round: + type: "string" + example: "0" + block_id: + required: + - "hash" + - "parts" + properties: + hash: + type: "string" + example: "112BC173FD838FB68EB43476816CD7B4C6661B6884A9E357B417EE957E1CF8F7" + parts: + required: + - "total" + - "hash" + properties: + total: + type: "string" + example: "1" + hash: + type: "string" + example: "38D4B26B5B725C4F13571EFE022C030390E4C33C8CF6F88EDD142EA769642DBD" + type: "object" + type: "object" + timestamp: + type: "string" + example: "2019-04-22T17:01:58.376629719Z" + validator_address: + type: "string" + example: "000001E443FD237E4B616E2FA69DF4EE3D49A94F" + validator_index: + type: "string" + example: "0" + signature: + type: "string" + example: "14jaTQXYRt8kbLKEhdHq7AXycrFImiLuZx50uOjs2+Zv+2i7RTG/jnObD07Jo2ubZ8xd7bNBJMqkgtkd0oQHAw==" + type: "object" + type: "object" + canonical: + type: "boolean" + example: true + type: "object" + ValidatorsResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "block_height" + - "validators" + properties: + block_height: + type: "string" + example: "55" + validators: + type: "array" + items: + type: "object" + properties: + address: + type: "string" + example: "000001E443FD237E4B616E2FA69DF4EE3D49A94F" + pub_key: + required: + - "type" + - "value" + properties: + type: + type: "string" + example: "tendermint/PubKeyEd25519" + value: + type: "string" + example: "9tK9IT+FPdf2qm+5c2qaxi10sWP+3erWTKgftn2PaQM=" + type: "object" + voting_power: + type: "string" + example: "250353" + proposer_priority: + type: "string" + example: "13769415" + type: "object" + GenesisResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "genesis" + properties: + genesis: + required: + - "genesis_time" + - "chain_id" + - "consensus_params" + - "validators" + - "app_hash" + properties: + genesis_time: + type: "string" + example: "2019-04-22T17:00:00Z" + chain_id: + type: "string" + example: "cosmoshub-2" + consensus_params: + required: + - "block" + - "evidence" + - "validator" + properties: + block: + required: + - "max_bytes" + - "max_gas" + - "time_iota_ms" + properties: + max_bytes: + type: "string" + example: "200000" + max_gas: + type: "string" + example: "2000000" + time_iota_ms: + type: "string" + example: "1000" + type: "object" + evidence: + required: + - "max_age" + properties: + max_age: + type: "string" + example: "1000000" + type: "object" + validator: + required: + - "pub_key_types" + properties: + pub_key_types: + type: "array" + items: + type: "string" + example: + - "ed25519" + type: "object" + type: "object" + validators: + type: "array" + items: + type: "object" + properties: + address: + type: "string" + example: "B00A6323737F321EB0B8D59C6FD497A14B60938A" + pub_key: + required: + - "type" + - "value" + properties: + type: + type: "string" + example: "tendermint/PubKeyEd25519" + value: + type: "string" + example: "cOQZvh/h9ZioSeUMZB/1Vy1Xo5x2sjrVjlE/qHnYifM=" + type: "object" + power: + type: "string" + example: "9328525" + name: + type: "string" + example: "Certus One" + app_hash: + type: "string" + example: "" + app_state: + properties: {} + type: "object" + type: "object" + type: "object" + DumpConsensusResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "round_state" + - "peers" + properties: + round_state: + required: + - "height" + - "round" + - "step" + - "start_time" + - "commit_time" + - "validators" + - "proposal" + - "proposal_block" + - "proposal_block_parts" + - "locked_round" + - "locked_block" + - "locked_block_parts" + - "valid_round" + - "valid_block" + - "valid_block_parts" + - "votes" + - "commit_round" + - "last_commit" + - "last_validators" + - "triggered_timeout_precommit" + properties: + height: + type: "string" + example: "1311801" + round: + type: "string" + example: "0" + step: + type: "number" + example: 3 + start_time: + type: "string" + example: "2019-08-05T11:28:49.064658805Z" + commit_time: + type: "string" + example: "2019-08-05T11:28:44.064658805Z" + validators: + required: + - "validators" + - "proposer" + properties: + validators: + type: "array" + items: + type: "object" + properties: + address: + type: "string" + example: "000001E443FD237E4B616E2FA69DF4EE3D49A94F" + pub_key: + required: + - "type" + - "value" + properties: + type: + type: "string" + example: "tendermint/PubKeyEd25519" + value: + type: "string" + example: "9tK9IT+FPdf2qm+5c2qaxi10sWP+3erWTKgftn2PaQM=" + type: "object" + voting_power: + type: "string" + example: "239727" + proposer_priority: + type: "string" + example: "-11896414" + proposer: + required: + - "address" + - "pub_key" + - "voting_power" + - "proposer_priority" + properties: + address: + type: "string" + example: "708FDDCE121CDADA502F2B0252FEF13FDAA31E50" + pub_key: + required: + - "type" + - "value" + properties: + type: + type: "string" + example: "tendermint/PubKeyEd25519" + value: + type: "string" + example: "VNMNfw7mrQBSpEvCtA9ykOe6BoR00RM9b/a9v3vXZhY=" + type: "object" + voting_power: + type: "string" + example: "295360" + proposer_priority: + type: "string" + example: "-88886833" + type: "object" + type: "object" + locked_round: + type: "string" + example: "-1" + valid_round: + type: "string" + example: "-1" + votes: + type: "array" + items: + type: "object" + properties: + round: + type: "string" + example: "0" + prevotes: + type: "array" + x-nullable: true + items: + type: "string" + example: + - "nil-Vote" + - "Vote{19:46A3F8B8393B 1311801/00/1(Prevote) 000000000000 64CE682305CB @ 2019-08-05T11:28:47.374703444Z}" + prevotes_bit_array: + type: "string" + example: "BA{100:___________________x________________________________________________________________________________} 209706/170220253 = 0.00" + precommits: + type: "array" + x-nullable: true + items: + type: "string" + example: + - "nil-Vote" + precommits_bit_array: + type: "string" + example: "BA{100:____________________________________________________________________________________________________} 0/170220253 = 0.00" + commit_round: + type: "string" + example: "-1" + last_commit: + x-nullable: true + required: + - "votes" + - "votes_bit_array" + - "peer_maj_23s" + properties: + votes: + type: "array" + items: + type: "string" + example: + - "Vote{0:000001E443FD 1311800/00/2(Precommit) 3071ADB27D1A 77EE1B6B6847 @ 2019-08-05T11:28:43.810128139Z}" + votes_bit_array: + type: "string" + example: "BA{100:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx} 170220253/170220253 = 1.00" + peer_maj_23s: + properties: {} + type: "object" + type: "object" + last_validators: + required: + - "validators" + - "proposer" + properties: + validators: + type: "array" + items: + type: "object" + properties: + address: + type: "string" + example: "000001E443FD237E4B616E2FA69DF4EE3D49A94F" + pub_key: + required: + - "type" + - "value" + properties: + type: + type: "string" + example: "tendermint/PubKeyEd25519" + value: + type: "string" + example: "9tK9IT+FPdf2qm+5c2qaxi10sWP+3erWTKgftn2PaQM=" + type: "object" + voting_power: + type: "string" + example: "239727" + proposer_priority: + type: "string" + example: "-12136141" + proposer: + required: + - "address" + - "pub_key" + - "voting_power" + - "proposer_priority" + properties: + address: + type: "string" + example: "B00A6323737F321EB0B8D59C6FD497A14B60938A" + pub_key: + required: + - "type" + - "value" + properties: + type: + type: "string" + example: "tendermint/PubKeyEd25519" + value: + type: "string" + example: "cOQZvh/h9ZioSeUMZB/1Vy1Xo5x2sjrVjlE/qHnYifM=" + type: "object" + voting_power: + type: "string" + example: "8590153" + proposer_priority: + type: "string" + example: "-79515145" + type: "object" + type: "object" + triggered_timeout_precommit: + type: "boolean" + example: false + type: "object" + peers: + type: "array" + items: + type: "object" + properties: + node_address: + type: "string" + example: "357f6a6c1d27414579a8185060aa8adf9815c43c@68.183.41.207:26656" + peer_state: + required: + - "round_state" + - "stats" + properties: + round_state: + required: + - "height" + - "round" + - "step" + - "start_time" + - "proposal" + - "proposal_block_parts_header" + - "proposal_block_parts" + - "proposal_pol_round" + - "proposal_pol" + - "prevotes" + - "precommits" + - "last_commit_round" + - "last_commit" + - "catchup_commit_round" + - "catchup_commit" + properties: + height: + type: "string" + example: "1311801" + round: + type: "string" + example: "0" + step: + type: "number" + example: 3 + start_time: + type: "string" + example: "2019-08-05T11:28:49.21730864Z" + proposal: + type: "boolean" + example: false + proposal_block_parts_header: + required: + - "total" + - "hash" + properties: + total: + type: "string" + example: "0" + hash: + type: "string" + example: "" + type: "object" + proposal_pol_round: + x-nullable: true + type: "string" + example: "-1" + proposal_pol: + x-nullable: true + type: "string" + example: "____________________________________________________________________________________________________" + prevotes: + x-nullable: true + type: "string" + example: "___________________x________________________________________________________________________________" + precommits: + x-nullable: true + type: "string" + example: "____________________________________________________________________________________________________" + last_commit_round: + x-nullable: true + type: "string" + example: "0" + last_commit: + x-nullable: true + type: "string" + example: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + catchup_commit_round: + type: "string" + x-nullable: true + example: "-1" + catchup_commit: + x-nullable: true + type: "string" + example: "____________________________________________________________________________________________________" + type: "object" + stats: + required: + - "votes" + - "block_parts" + properties: + votes: + type: "string" + example: "1159558" + block_parts: + type: "string" + example: "4786" + type: "object" + type: "object" + type: "object" + ConsensusStateResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "round_state" + properties: + round_state: + required: + - "height/round/step" + - "start_time" + - "proposal_block_hash" + - "locked_block_hash" + - "valid_block_hash" + - "height_vote_set" + properties: + height/round/step: + type: "string" + example: "1262197/0/8" + start_time: + type: "string" + example: "2019-08-01T11:52:38.962730289Z" + proposal_block_hash: + type: "string" + example: "634ADAF1F402663BEC2ABC340ECE8B4B45AA906FA603272ACC5F5EED3097E009" + locked_block_hash: + type: "string" + example: "634ADAF1F402663BEC2ABC340ECE8B4B45AA906FA603272ACC5F5EED3097E009" + valid_block_hash: + type: "string" + example: "634ADAF1F402663BEC2ABC340ECE8B4B45AA906FA603272ACC5F5EED3097E009" + height_vote_set: + type: "array" + items: + type: "object" + properties: + round: + type: "string" + example: "0" + prevotes: + type: "array" + items: + type: "string" + example: + - "Vote{0:000001E443FD 1262197/00/1(Prevote) 634ADAF1F402 7BB974E1BA40 @ 2019-08-01T11:52:35.513572509Z}" + - "nil-Vote" + prevotes_bit_array: + type: "string" + example: "BA{100:xxxxxxxxxxxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx} 169753436/170151262 = 1.00" + precommits: + type: "array" + items: + type: "string" + example: + - "Vote{5:18C78D135C9D 1262197/00/2(Precommit) 634ADAF1F402 8B5EFFFEABCD @ 2019-08-01T11:52:36.25600005Z}" + - "nil-Vote" + precommits_bit_array: + type: "string" + example: "BA{100:xxxxxx_xxxxx_xxxx_x_xxx_xx_xx_xx__x_x_x__xxxxxxxxxxxxxx_xxxx_xx_xxxxxx_xxxxxxxx_xxxx_xxx_x_xxxx__xxx} 118726247/170151262 = 0.70" + type: "object" + type: "object" + ConsensusParamsResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "block_height" + - "consensus_params" + properties: + block_height: + type: "string" + example: "1313448" + consensus_params: + required: + - "block" + - "evidence" + - "validator" + properties: + block: + required: + - "max_bytes" + - "max_gas" + - "time_iota_ms" + properties: + max_bytes: + type: "string" + example: "200000" + max_gas: + type: "string" + example: "2000000" + time_iota_ms: + type: "string" + example: "1000" + type: "object" + evidence: + required: + - "max_age" + properties: + max_age: + type: "string" + example: "1000000" + type: "object" + validator: + required: + - "pub_key_types" + properties: + pub_key_types: + type: "array" + items: + type: "string" + example: + - "ed25519" + type: "object" + type: "object" + type: "object" + UnconfirmedTransactionsResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "n_txs" + - "total" + - "total_bytes" +# - "txs" + properties: + n_txs: + type: "string" + example: "31" + total: + type: "string" + example: "82" + total_bytes: + type: "string" + example: "19974" +# txs: +# type: "array" +# x-nullable: true +# items: +# type: "string" +# x-nullable: true +# example: +# - "gAPwYl3uCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUA75/FmYq9WymsOBJ0XSJ8yV8zmQKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhQbrvwbvlNiT+Yjr86G+YQNx7kRVgowjE1xDQoUjJyJG+WaWBwSiGannBRFdrbma+8SFK2m+1oxgILuQLO55n8mWfnbIzyPCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUQNGfkmhTNMis4j+dyMDIWXdIPiYKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhS8sL0D0wwgGCItQwVowak5YB38KRIUCg4KBXVhdG9tEgUxMDA1NBDoxRgaagom61rphyECn8x7emhhKdRCB2io7aS/6Cpuq5NbVqbODmqOT3jWw6kSQKUresk+d+Gw0BhjiggTsu8+1voW+VlDCQ1GRYnMaFOHXhyFv7BCLhFWxLxHSAYT8a5XqoMayosZf9mANKdXArA=" + type: "object" + NumUnconfirmedTransactionsResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "n_txs" + - "total" + - "total_bytes" + - "txs" + properties: + n_txs: + type: "string" + example: "82" + total: + type: "string" + example: "82" + total_bytes: + type: "string" + example: "19974" + txs: + type: array + x-nullable: true + items: + type: string + x-nullable: true + example: + - null + - "gAPwYl3uCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUA75/FmYq9WymsOBJ0XSJ8yV8zmQKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhQbrvwbvlNiT+Yjr86G+YQNx7kRVgowjE1xDQoUjJyJG+WaWBwSiGannBRFdrbma+8SFK2m+1oxgILuQLO55n8mWfnbIzyPCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUQNGfkmhTNMis4j+dyMDIWXdIPiYKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhS8sL0D0wwgGCItQwVowak5YB38KRIUCg4KBXVhdG9tEgUxMDA1NBDoxRgaagom61rphyECn8x7emhhKdRCB2io7aS/6Cpuq5NbVqbODmqOT3jWw6kSQKUresk+d+Gw0BhjiggTsu8+1voW+VlDCQ1GRYnMaFOHXhyFv7BCLhFWxLxHSAYT8a5XqoMayosZf9mANKdXArA=" + type: "object" + TxSearchResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "txs" + - "total_count" + properties: + txs: + type: "array" + items: + type: "object" + properties: + hash: + type: "string" + example: "D70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED" + height: + type: "string" + example: "1000" + index: + type: "number" + example: 0 + tx_result: + required: + - "log" + - "gasWanted" + - "gasUsed" + - "tags" + properties: + log: + type: "string" + example: "[{\"msg_index\":\"0\",\"success\":true,\"log\":\"\"}]" + gasWanted: + type: "string" + example: "200000" + gasUsed: + type: "string" + example: "28596" + tags: + type: "array" + items: + type: "object" + properties: + key: + type: "string" + example: "YWN0aW9u" + value: + type: "string" + example: "c2VuZA==" + type: "object" + tx: + type: "string" + example: "5wHwYl3uCkaoo2GaChQmSIu8hxpJxLcCuIi8fiHN4TMwrRIU/Af1cEG7Rcs/6LjTl7YjRSymJfYaFAoFdWF0b20SCzE0OTk5OTk1MDAwEhMKDQoFdWF0b20SBDUwMDAQwJoMGmoKJuta6YchAwswBShaB1wkZBctLIhYqBC3JrAI28XGzxP+rVEticGEEkAc+khTkKL9CDE47aDvjEHvUNt+izJfT4KVF2v2JkC+bmlH9K08q3PqHeMI9Z5up+XMusnTqlP985KF+SI5J3ZOIhhNYWRlIGJ5IENpcmNsZSB3aXRoIGxvdmU=" + proof: + required: + - "RootHash" + - "Data" + - "Proof" + properties: + RootHash: + type: "string" + example: "72FE6BF6D4109105357AECE0A82E99D0F6288854D16D8767C5E72C57F876A14D" + Data: + type: "string" + example: "5wHwYl3uCkaoo2GaChQmSIu8hxpJxLcCuIi8fiHN4TMwrRIU/Af1cEG7Rcs/6LjTl7YjRSymJfYaFAoFdWF0b20SCzE0OTk5OTk1MDAwEhMKDQoFdWF0b20SBDUwMDAQwJoMGmoKJuta6YchAwswBShaB1wkZBctLIhYqBC3JrAI28XGzxP+rVEticGEEkAc+khTkKL9CDE47aDvjEHvUNt+izJfT4KVF2v2JkC+bmlH9K08q3PqHeMI9Z5up+XMusnTqlP985KF+SI5J3ZOIhhNYWRlIGJ5IENpcmNsZSB3aXRoIGxvdmU=" + Proof: + required: + - "total" + - "index" + - "leaf_hash" + - "aunts" + properties: + total: + type: "string" + example: "2" + index: + type: "string" + example: "0" + leaf_hash: + type: "string" + example: "eoJxKCzF3m72Xiwb/Q43vJ37/2Sx8sfNS9JKJohlsYI=" + aunts: + type: "array" + items: + type: "string" + example: + - "eWb+HG/eMmukrQj4vNGyFYb3nKQncAWacq4HF5eFzDY=" + type: "object" + type: "object" + total_count: + type: "string" + example: "2" + type: "object" + TxResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "hash" + - "height" + - "index" + - "tx_result" + - "tx" + properties: + hash: + type: "string" + example: "D70952032620CC4E2737EB8AC379806359D8E0B17B0488F627997A0B043ABDED" + height: + type: "string" + example: "1000" + index: + type: "number" + example: 0 + tx_result: + required: + - "log" + - "gasWanted" + - "gasUsed" + - "tags" + properties: + log: + type: "string" + example: "[{\"msg_index\":\"0\",\"success\":true,\"log\":\"\"}]" + gasWanted: + type: "string" + example: "200000" + gasUsed: + type: "string" + example: "28596" + tags: + type: "array" + items: + type: "object" + properties: + key: + type: "string" + example: "YWN0aW9u" + value: + type: "string" + example: "c2VuZA==" + type: "object" + tx: + type: "string" + example: "5wHwYl3uCkaoo2GaChQmSIu8hxpJxLcCuIi8fiHN4TMwrRIU/Af1cEG7Rcs/6LjTl7YjRSymJfYaFAoFdWF0b20SCzE0OTk5OTk1MDAwEhMKDQoFdWF0b20SBDUwMDAQwJoMGmoKJuta6YchAwswBShaB1wkZBctLIhYqBC3JrAI28XGzxP+rVEticGEEkAc+khTkKL9CDE47aDvjEHvUNt+izJfT4KVF2v2JkC+bmlH9K08q3PqHeMI9Z5up+XMusnTqlP985KF+SI5J3ZOIhhNYWRlIGJ5IENpcmNsZSB3aXRoIGxvdmU=" + type: "object" + ABCIInfoResponse: + type: object + required: + - "jsonrpc" + - "id" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "response" + properties: + response: + required: + - "data" + - "app_version" + - "version" + properties: + data: + type: "string" + example: "{\"size\":0}" + version: + type: string + example: "0.16.1" + app_version: + type: "string" + example: "1314126" + type: "object" + type: "object" + ABCIQueryResponse: + type: object + required: + - "error" + - "result" + - "id" + - "jsonrpc" + properties: + error: + type: "string" + example: "" + result: + required: + - "response" + properties: + response: + required: + - "log" + - "height" + - "proof" + - "value" + - "key" + - "index" + - "code" + properties: + log: + type: "string" + example: "exists" + height: + type: "string" + example: "0" + proof: + type: "string" + example: "010114FED0DAD959F36091AD761C922ABA3CBF1D8349990101020103011406AA2262E2F448242DF2C2607C3CDC705313EE3B0001149D16177BC71E445476174622EA559715C293740C" + value: + type: "string" + example: "61626364" + key: + type: "string" + example: "61626364" + index: + type: "string" + example: "-1" + code: + type: "string" + example: "0" + type: "object" + type: "object" + id: + type: "string" + example: "" + jsonrpc: + type: "string" + example: "2.0" + BroadcastEvidenceResponse: + type: object + required: + - "id" + - "jsonrpc" + properties: + error: + type: "string" + example: "" + result: + type: "string" + example: "" + id: + type: "string" + example: "" + jsonrpc: + type: "string" + example: "2.0" + BroadcastTxCommitResponse: + type: object + required: + - "error" + - "result" + - "id" + - "jsonrpc" + properties: + error: + type: "string" + example: "" + result: + required: + - "height" + - "hash" + - "deliver_tx" + - "check_tx" + properties: + height: + type: "string" + example: "26682" + hash: + type: "string" + example: "75CA0F856A4DA078FC4911580360E70CEFB2EBEE" + deliver_tx: + required: + - "log" + - "data" + - "code" + properties: + log: + type: "string" + example: "" + data: + type: "string" + example: "" + code: + type: "string" + example: "0" + type: "object" + check_tx: + required: + - "log" + - "data" + - "code" + properties: + log: + type: "string" + example: "" + data: + type: "string" + example: "" + code: + type: "string" + example: "0" + type: "object" + type: "object" + id: + type: "string" + example: "" + jsonrpc: + type: "string" + example: "2.0" + BroadcastTxResponse: + type: object + required: + - "jsonrpc" + - "id" + - "result" + - "error" + properties: + jsonrpc: + type: "string" + example: "2.0" + id: + type: "string" + example: "" + result: + required: + - "code" + - "data" + - "log" + - "hash" + properties: + code: + type: "string" + example: "0" + data: + type: "string" + example: "" + log: + type: "string" + example: "" + hash: + type: "string" + example: "0D33F2F03A5234F38706E43004489E061AC40A2E" + type: "object" + error: + type: "string" + example: "" diff --git a/dredd.yml b/dredd.yml new file mode 100644 index 000000000..0db3d767d --- /dev/null +++ b/dredd.yml @@ -0,0 +1,33 @@ +color: true +dry-run: null +hookfiles: build/contract_tests +language: go +require: null +server: make localnet-start +server-wait: 30 +init: false +custom: {} +names: false +only: [] +reporter: [] +output: [] +header: [] +sorted: false +user: null +inline-errors: false +details: false +method: [GET] +loglevel: warning +path: [] +hooks-worker-timeout: 5000 +hooks-worker-connect-timeout: 1500 +hooks-worker-connect-retry: 500 +hooks-worker-after-connect-wait: 100 +hooks-worker-term-timeout: 5000 +hooks-worker-term-retry: 500 +hooks-worker-handler-host: 127.0.0.1 +hooks-worker-handler-port: 61321 +config: ./dredd.yml +# This path accepts no variables +blueprint: ./docs/spec/rpc/swagger.yaml +endpoint: 'http://127.0.0.1:26657/' diff --git a/go.mod b/go.mod index ae5fa8e21..f64d603f4 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d // indirect github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 github.com/rs/cors v1.6.0 + github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa github.com/spf13/afero v1.1.2 // indirect github.com/spf13/cast v1.3.0 // indirect github.com/spf13/cobra v0.0.1 diff --git a/go.sum b/go.sum index 8924eebe3..2bae0430d 100644 --- a/go.sum +++ b/go.sum @@ -94,6 +94,8 @@ github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFE github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa h1:YJfZp12Z3AFhSBeXOlv4BO55RMwPn2NoQeDsrdWnBtY= +github.com/snikch/goodman v0.0.0-20171125024755-10e37e294daa/go.mod h1:oJyF+mSPHbB5mVY2iO9KV3pTt/QbIkGaO8gQ2WrDbP4= github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= diff --git a/scripts/get_nodejs.sh b/scripts/get_nodejs.sh new file mode 100755 index 000000000..7f0dd6e38 --- /dev/null +++ b/scripts/get_nodejs.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash + +VERSION=v11.15.0 +NODE_FULL=node-${VERSION}-linux-x64 + +mkdir -p ~/.local/bin +mkdir -p ~/.local/node +wget http://nodejs.org/dist/${VERSION}/${NODE_FULL}.tar.gz -O ~/.local/node/${NODE_FULL}.tar.gz +tar -xzf ~/.local/node/${NODE_FULL}.tar.gz -C ~/.local/node/ +ln -s ~/.local/node/${NODE_FULL}/bin/node ~/.local/bin/node +ln -s ~/.local/node/${NODE_FULL}/bin/npm ~/.local/bin/npm +export PATH=~/.local/bin:$PATH +npm i -g dredd@11.0.1 +ln -s ~/.local/node/${NODE_FULL}/bin/dredd ~/.local/bin/dredd diff --git a/scripts/get_tools.sh b/scripts/get_tools.sh index d8c17df11..4dfb454bd 100755 --- a/scripts/get_tools.sh +++ b/scripts/get_tools.sh @@ -64,3 +64,4 @@ installFromGithub golangci/golangci-lint 7b2421d55194c9dc385eff7720a037aa9244ca3 installFromGithub petermattis/goid b0b1615b78e5ee59739545bb38426383b2cda4c9 installFromGithub sasha-s/go-deadlock d68e2bc52ae3291765881b9056f2c1527f245f1e go get golang.org/x/tools/cmd/goimports +installFromGithub snikch/goodman 10e37e294daa3c9a90abded60ff9924bafab3888 cmd/goodman From b8922bbff9d520415d405e249ff3cc6649beeb0f Mon Sep 17 00:00:00 2001 From: Marko Date: Fri, 16 Aug 2019 15:45:19 +0200 Subject: [PATCH 180/211] remove mentions of go-wire (#3904) * Cleanup go-wire mentions * remove Roadmap.md * build_race in makefile was failing ref #2790 --- Makefile | 2 +- ROADMAP.md | 23 ----------------------- abci/types/protoreplace/protoreplace.go | 2 +- consensus/state.go | 2 +- docs/spec/p2p/connection.md | 2 +- docs/spec/reactors/mempool/messages.md | 6 +++--- rpc/core/routes.go | 2 +- types/results.go | 2 +- 8 files changed, 9 insertions(+), 32 deletions(-) delete mode 100644 ROADMAP.md diff --git a/Makefile b/Makefile index a5681cf34..e5ec00112 100644 --- a/Makefile +++ b/Makefile @@ -28,7 +28,7 @@ build_c: CGO_ENABLED=1 go build $(BUILD_FLAGS) -tags "$(BUILD_TAGS) cleveldb" -o $(OUTPUT) ./cmd/tendermint/ build_race: - CGO_ENABLED=0 go build -race $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o $(OUTPUT) ./cmd/tendermint + CGO_ENABLED=1 go build -race $(BUILD_FLAGS) -tags $(BUILD_TAGS) -o $(OUTPUT) ./cmd/tendermint install: CGO_ENABLED=0 go install $(BUILD_FLAGS) -tags $(BUILD_TAGS) ./cmd/tendermint diff --git a/ROADMAP.md b/ROADMAP.md deleted file mode 100644 index 60c284333..000000000 --- a/ROADMAP.md +++ /dev/null @@ -1,23 +0,0 @@ -# Roadmap - -BREAKING CHANGES: -- Better support for injecting randomness -- Upgrade consensus for more real-time use of evidence - -FEATURES: -- Use the chain as its own CA for nodes and validators -- Tooling to run multiple blockchains/apps, possibly in a single process -- State syncing (without transaction replay) -- Add authentication and rate-limitting to the RPC - -IMPROVEMENTS: -- Improve subtleties around mempool caching and logic -- Consensus optimizations: - - cache block parts for faster agreement after round changes - - propagate block parts rarest first -- Better testing of the consensus state machine (ie. use a DSL) -- Auto compiled serialization/deserialization code instead of go-wire reflection - -BUG FIXES: -- Graceful handling/recovery for apps that have non-determinism or fail to halt -- Graceful handling/recovery for violations of safety, or liveness diff --git a/abci/types/protoreplace/protoreplace.go b/abci/types/protoreplace/protoreplace.go index 3ea0c73da..7058a70fb 100644 --- a/abci/types/protoreplace/protoreplace.go +++ b/abci/types/protoreplace/protoreplace.go @@ -40,7 +40,7 @@ func main() { } if writeImportTime && !wroteImport { wroteImport = true - fmt.Fprintf(outFile, "import \"github.com/tendermint/go-wire/data\"\n") + fmt.Fprintf(outFile, "import \"github.com/tendermint/go-amino/data\"\n") } if gotPackageLine { diff --git a/consensus/state.go b/consensus/state.go index af7742f6a..85b45b0ea 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -1678,7 +1678,7 @@ func (cs *ConsensusState) addVote(vote *types.Vote, peerID p2p.ID) (added bool, } default: - panic(fmt.Sprintf("Unexpected vote type %X", vote.Type)) // go-wire should prevent this. + panic(fmt.Sprintf("Unexpected vote type %X", vote.Type)) // go-amino should prevent this. } return diff --git a/docs/spec/p2p/connection.md b/docs/spec/p2p/connection.md index 47366a549..fd2e7bc4d 100644 --- a/docs/spec/p2p/connection.md +++ b/docs/spec/p2p/connection.md @@ -61,7 +61,7 @@ func (m MConnection) TrySend(chID byte, msg interface{}) bool {} `Send(chID, msg)` is a blocking call that waits until `msg` is successfully queued for the channel with the given id byte `chID`. The message `msg` is serialized -using the `tendermint/wire` submodule's `WriteBinary()` reflection routine. +using the `tendermint/go-amino` submodule's `WriteBinary()` reflection routine. `TrySend(chID, msg)` is a nonblocking call that queues the message msg in the channel with the given id byte chID if the queue is not full; otherwise it returns false immediately. diff --git a/docs/spec/reactors/mempool/messages.md b/docs/spec/reactors/mempool/messages.md index 117fc5f2f..9c583ac0f 100644 --- a/docs/spec/reactors/mempool/messages.md +++ b/docs/spec/reactors/mempool/messages.md @@ -13,13 +13,13 @@ type TxMessage struct { } ``` -TxMessage is go-wire encoded and prepended with `0x1` as a -"type byte". This is followed by a go-wire encoded byte-slice. +TxMessage is go-amino encoded and prepended with `0x1` as a +"type byte". This is followed by a go-amino encoded byte-slice. Prefix of 40=0x28 byte tx is: `0x010128...` followed by the actual 40-byte tx. Prefix of 350=0x015e byte tx is: `0x0102015e...` followed by the actual 350 byte tx. -(Please see the [go-wire repo](https://github.com/tendermint/go-wire#an-interface-example) for more information) +(Please see the [go-amino repo](https://github.com/tendermint/go-amino#an-interface-example) for more information) ## RPC Messages diff --git a/rpc/core/routes.go b/rpc/core/routes.go index 7ca69f29e..d44ce257a 100644 --- a/rpc/core/routes.go +++ b/rpc/core/routes.go @@ -5,7 +5,7 @@ import ( ) // TODO: better system than "unsafe" prefix -// NOTE: Amino is registered in rpc/core/types/wire.go. +// NOTE: Amino is registered in rpc/core/types/codec.go. var Routes = map[string]*rpc.RPCFunc{ // subscribe/unsubscribe are reserved for websocket events. "subscribe": rpc.NewWSRPCFunc(Subscribe, "query"), diff --git a/types/results.go b/types/results.go index d7d82d894..6b0c37562 100644 --- a/types/results.go +++ b/types/results.go @@ -41,7 +41,7 @@ func NewResultFromResponse(response *abci.ResponseDeliverTx) ABCIResult { } } -// Bytes serializes the ABCIResponse using wire +// Bytes serializes the ABCIResponse using amino func (a ABCIResults) Bytes() []byte { bz, err := cdc.MarshalBinaryLengthPrefixed(a) if err != nil { From 297cf02f533e93e074be4b2e53f0b6eb1db7e1b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gustavo=20Cha=C3=ADn?= Date: Thu, 22 Aug 2019 11:23:07 +0200 Subject: [PATCH 181/211] rpc: protect subscription access from race condition (#3910) When using the RPC client in my test suite (with -race enabled), I do a lot of Subscribe/Unsubscribe operations, at some point (randomly) the race detector returns the following warning: WARNING: DATA RACE Read at 0x00c0009dbe30 by goroutine 31: runtime.mapiterinit() /usr/local/go/src/runtime/map.go:804 +0x0 github.com/tendermint/tendermint/rpc/client.(*WSEvents).redoSubscriptionsAfter() /go/pkg/mod/github.com/tendermint/tendermint@v0.31.5/rpc/client/httpclient.go:364 +0xc0 github.com/tendermint/tendermint/rpc/client.(*WSEvents).eventListener() /go/pkg/mod/github.com/tendermint/tendermint@v0.31.5/rpc/client/httpclient.go:393 +0x3c6 Turns out that the redoSubscriptionAfter is not protecting the access to subscriptions. The following change protects the read access to the subscription map behind the mutex --- CHANGELOG_PENDING.md | 1 + rpc/client/httpclient.go | 2 ++ 2 files changed, 3 insertions(+) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index fd0ea50e2..2912b63b6 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -22,3 +22,4 @@ program](https://hackerone.com/tendermint). - [config] \#3868 move misplaced `max_msg_bytes` into mempool section - [store] \#3893 register block amino, not just crypto +- [rpc] [\#3910](https://github.com/tendermint/tendermint/pull/3910) protect subscription access from race conditions (@gchaincl) diff --git a/rpc/client/httpclient.go b/rpc/client/httpclient.go index aa9b81991..671d2425d 100644 --- a/rpc/client/httpclient.go +++ b/rpc/client/httpclient.go @@ -462,6 +462,8 @@ func (w *WSEvents) UnsubscribeAll(ctx context.Context, subscriber string) error func (w *WSEvents) redoSubscriptionsAfter(d time.Duration) { time.Sleep(d) + w.mtx.RLock() + defer w.mtx.RUnlock() for q := range w.subscriptions { err := w.ws.Subscribe(context.Background(), q) if err != nil { From 5d9c4f8e0a1a5807ba0bba767bde5442850c4f81 Mon Sep 17 00:00:00 2001 From: Ethan Buchman Date: Fri, 23 Aug 2019 04:11:26 -0400 Subject: [PATCH 182/211] docs: fix some typos and changelog entries (#3915) Fixes some typos in the docs. Fixes the classification of some changelog items re Features vs. Improvements --- CHANGELOG.md | 8 ++++---- docs/spec/abci/apps.md | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d79ae8281..8ba24a6dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,20 +17,20 @@ program](https://hackerone.com/tendermint). ### FEATURES: +- [blockchain] [\#3561](https://github.com/tendermint/tendermint/issues/3561) Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) +- [mempool] [\#3826](https://github.com/tendermint/tendermint/issues/3826) Make `max_msg_bytes` configurable(@bluele) - [node] [\#3846](https://github.com/tendermint/tendermint/pull/3846) Allow replacing existing p2p.Reactor(s) using [`CustomReactors` option](https://godoc.org/github.com/tendermint/tendermint/node#CustomReactors). Warning: beware of accidental name clashes. Here is the list of existing reactors: MEMPOOL, BLOCKCHAIN, CONSENSUS, EVIDENCE, PEX. -- [p2p] [\#3834](https://github.com/tendermint/tendermint/issues/3834) Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file - [rpc] [\#3818](https://github.com/tendermint/tendermint/issues/3818) Make `max_body_bytes` and `max_header_bytes` configurable(@bluele) -- [mempool] [\#3826](https://github.com/tendermint/tendermint/issues/3826) Make `max_msg_bytes` configurable(@bluele) -- [blockchain] [\#3561](https://github.com/tendermint/tendermint/issues/3561) Add early version of the new blockchain reactor, which is supposed to be more modular and testable compared to the old version. To try it, you'll have to change `version` in the config file, [here](https://github.com/tendermint/tendermint/blob/master/config/toml.go#L303) NOTE: It's not ready for a production yet. For further information, see [ADR-40](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-040-blockchain-reactor-refactor.md) & [ADR-43](https://github.com/tendermint/tendermint/blob/master/docs/architecture/adr-043-blockchain-riri-org.md) +- [rpc] [\#2252](https://github.com/tendermint/tendermint/issues/2252) Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence ### IMPROVEMENTS: - [abci] [\#3809](https://github.com/tendermint/tendermint/issues/3809) Recover from application panics in `server/socket_server.go` to allow socket cleanup (@ruseinov) -- [rpc] [\#2252](https://github.com/tendermint/tendermint/issues/2252) Add `/broadcast_evidence` endpoint to submit double signing and other types of evidence - [p2p] [\#3664](https://github.com/tendermint/tendermint/issues/3664) p2p/conn: reuse buffer when write/read from secret connection(@guagualvcha) +- [p2p] [\#3834](https://github.com/tendermint/tendermint/issues/3834) Do not write 'Couldn't connect to any seeds' error log if there are no seeds in config file - [rpc] [\#3076](https://github.com/tendermint/tendermint/issues/3076) Improve transaction search performance ### BUG FIXES: diff --git a/docs/spec/abci/apps.md b/docs/spec/abci/apps.md index 92914ac64..4fe0389e4 100644 --- a/docs/spec/abci/apps.md +++ b/docs/spec/abci/apps.md @@ -66,7 +66,7 @@ After `Commit`, CheckTx is run again on all transactions that remain in the node's local mempool after filtering those included in the block. To prevent the mempool from rechecking all transactions every time a block is committed, set the configuration option `mempool.recheck=false`. As of Tendermint v0.32.1, -an additional `Type` parameter is made available to the CheckTx function that +an additional `Type` parameter is made available to the CheckTx function that indicates whether an incoming transaction is new (`CheckTxType_New`), or a recheck (`CheckTxType_Recheck`). @@ -211,7 +211,7 @@ message PubKey { The `pub_key` currently supports only one type: -- `type = "ed25519" and`data = ` +- `type = "ed25519"` and `data = ` The `power` is the new voting power for the validator, with the following rules: From 0db122130b23d5f939937d2153c091bf58594095 Mon Sep 17 00:00:00 2001 From: Marko Date: Mon, 26 Aug 2019 14:27:33 +0200 Subject: [PATCH 183/211] Test nodejs image (#3921) * Test nodejs image Signed-off-by: Marko Baricevic * minor changes * v in version * version in swagger.yaml --- .circleci/config.yml | 14 +-- CONTRIBUTING.md | 53 ++++----- docs/spec/rpc/swagger.yaml | 237 ++++++++++++++++++------------------- scripts/get_nodejs.sh | 4 +- 4 files changed, 153 insertions(+), 155 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index d350d8b7c..be2d9690b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -216,11 +216,11 @@ jobs: name: Trigger website build command: | curl --silent \ - --show-error \ - -X POST \ - --header "Content-Type: application/json" \ - -d "{\"branch\": \"$CIRCLE_BRANCH\"}" \ - "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$WEBSITE_REPO_NAME/build?circle-token=$TENDERBOT_API_TOKEN" > response.json + --show-error \ + -X POST \ + --header "Content-Type: application/json" \ + -d "{\"branch\": \"$CIRCLE_BRANCH\"}" \ + "https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$WEBSITE_REPO_NAME/build?circle-token=$TENDERBOT_API_TOKEN" > response.json RESULT=`jq -r '.status' response.json` MESSAGE=`jq -r '.message' response.json` @@ -373,12 +373,12 @@ jobs: steps: - checkout - run: - name: Test RPC endpoints agsainst swagger documentation + name: Test RPC endpoints against swagger documentation command: | set -x export PATH=~/.local/bin:$PATH - # install node 11.15 and dredd + # install node and dredd ./scripts/get_nodejs.sh # build the binaries with a proper version of Go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63c4de646..1b9ea4409 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ Thank you for considering making contributions to Tendermint and related repositories! Start by taking a look at the [coding repo](https://github.com/tendermint/coding) for overall information on repository workflow and standards. -Please follow standard github best practices: fork the repo, branch from the tip of `master`, make some commits, and submit a pull request to `master`. +Please follow standard github best practices: fork the repo, branch from the tip of `master`, make some commits, and submit a pull request to `master`. See the [open issues](https://github.com/tendermint/tendermint/issues) for things we need help with! Before making a pull request, please open an issue describing the @@ -21,16 +21,16 @@ Please make sure to use `gofmt` before every commit - the easiest way to do this Please note that Go requires code to live under absolute paths, which complicates forking. While my fork lives at `https://github.com/ebuchman/tendermint`, -the code should never exist at `$GOPATH/src/github.com/ebuchman/tendermint`. +the code should never exist at `$GOPATH/src/github.com/ebuchman/tendermint`. Instead, we use `git remote` to add the fork as a new remote for the original repo, -`$GOPATH/src/github.com/tendermint/tendermint `, and do all the work there. +`$GOPATH/src/github.com/tendermint/tendermint`, and do all the work there. For instance, to create a fork and work on a branch of it, I would: - * Create the fork on github, using the fork button. - * Go to the original repo checked out locally (i.e. `$GOPATH/src/github.com/tendermint/tendermint`) - * `git remote rename origin upstream` - * `git remote add origin git@github.com:ebuchman/basecoin.git` +- Create the fork on github, using the fork button. +- Go to the original repo checked out locally (i.e. `$GOPATH/src/github.com/tendermint/tendermint`) +- `git remote rename origin upstream` +- `git remote add origin git@github.com:ebuchman/basecoin.git` Now `origin` refers to my fork and `upstream` refers to the tendermint version. So I can `git push -u origin master` to update my fork, and make pull requests to tendermint from there. @@ -38,8 +38,8 @@ Of course, replace `ebuchman` with your git handle. To pull in updates from the origin repo, run - * `git fetch upstream` - * `git rebase upstream/master` (or whatever branch you want) +- `git fetch upstream` +- `git rebase upstream/master` (or whatever branch you want) ## Dependencies @@ -113,7 +113,7 @@ removed from the header in rpc responses as well. ## Branching Model and Release -The main development branch is master. +The main development branch is master. Every release is maintained in a release branch named `vX.Y.Z`. @@ -140,36 +140,35 @@ easy to reference the pull request where a change was introduced. #### Major Release -1. start on `master` +1. start on `master` 2. run integration tests (see `test_integrations` in Makefile) 3. prepare release in a pull request against `master` (to be squash merged): - - copy `CHANGELOG_PENDING.md` to top of `CHANGELOG.md` - - run `python ./scripts/linkify_changelog.py CHANGELOG.md` to add links for - all issues - - run `bash ./scripts/authors.sh` to get a list of authors since the latest - release, and add the github aliases of external contributors to the top of - the changelog. To lookup an alias from an email, try `bash - ./scripts/authors.sh ` - - reset the `CHANGELOG_PENDING.md` - - bump versions + - copy `CHANGELOG_PENDING.md` to top of `CHANGELOG.md` + - run `python ./scripts/linkify_changelog.py CHANGELOG.md` to add links for + all issues + - run `bash ./scripts/authors.sh` to get a list of authors since the latest + release, and add the github aliases of external contributors to the top of + the changelog. To lookup an alias from an email, try `bash ./scripts/authors.sh ` + - reset the `CHANGELOG_PENDING.md` + - bump versions 4. push your changes with prepared release details to `vX.X` (this will trigger the release `vX.X.0`) 5. merge back to master (don't squash merge!) #### Minor Release -If there were no breaking changes and you need to create a release nonetheless, -the procedure is almost exactly like with a new release above. +If there were no breaking changes and you need to create a release nonetheless, +the procedure is almost exactly like with a new release above. The only difference is that in the end you create a pull request against the existing `X.X` branch. The branch name should match the release number you want to create. -Merging this PR will trigger the next release. -For example, if the PR is against an existing 0.34 branch which already contains a v0.34.0 release/tag, +Merging this PR will trigger the next release. +For example, if the PR is against an existing 0.34 branch which already contains a v0.34.0 release/tag, the patch version will be incremented and the created release will be v0.34.1. #### Backport Release 1. start from the existing release branch you want to backport changes to (e.g. v0.30) -Branch to a release/vX.X.X branch locally (e.g. release/v0.30.7) + Branch to a release/vX.X.X branch locally (e.g. release/v0.30.7) 2. cherry pick the commit(s) that contain the changes you want to backport (usually these commits are from squash-merged PRs which were already reviewed) 3. steps 2 and 3 from [Major Release](#major-release) 4. push changes to release/vX.X.X branch @@ -187,12 +186,12 @@ includes its continuous integration status using a badge in the `README.md`. ### RPC Testing If you contribute to the RPC endpoints it's important to document your changes in the [Swagger file](./docs/spec/rpc/swagger.yaml) -To test your changes you should install `nodejs` version `v11.15.*` and run: +To test your changes you should install `nodejs` and run: ```bash npm i -g dredd make build-linux build-contract-tests-hooks make contract-tests -``` +``` This command will popup a network and check every endpoint against what has been documented diff --git a/docs/spec/rpc/swagger.yaml b/docs/spec/rpc/swagger.yaml index 70fa82014..a276304a1 100644 --- a/docs/spec/rpc/swagger.yaml +++ b/docs/spec/rpc/swagger.yaml @@ -1,6 +1,6 @@ swagger: "2.0" info: - version: "0.32.1" + version: "Master" title: RPC client for Tendermint description: A REST interface for state queries, transaction generation and broadcasting. license: @@ -37,12 +37,12 @@ paths: If you haven't received anything after a couple of blocks, resend it. If the same happens again, send it to some other node. A few reasons why it could happen: - + 1. malicious node can drop or pretend it had committed your tx 2. malicious proposer (not necessary the one you're communicating with) can drop transactions, which might become valid in the future (https://github.com/tendermint/tendermint/issues/3322) - + Please refer to https://tendermint.com/docs/tendermint-core/using-tendermint.html#formatting @@ -60,11 +60,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/BroadcastTxResponse' + $ref: "#/definitions/BroadcastTxResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /broadcast_tx_async: get: summary: Returns right away, with no response. Does not wait for CheckTx nor DeliverTx results. @@ -101,11 +101,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/BroadcastTxResponse' + $ref: "#/definitions/BroadcastTxResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /broadcast_tx_commit: get: summary: Returns with the responses from CheckTx and DeliverTx. @@ -140,11 +140,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/BroadcastTxCommitResponse' + $ref: "#/definitions/BroadcastTxCommitResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /subscribe: get: summary: Subscribe for events via WebSocket. @@ -285,11 +285,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/EmptyResponse' + $ref: "#/definitions/EmptyResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /unsubscribe: get: summary: Unsubscribe from event on Websocket @@ -297,19 +297,19 @@ paths: - Websocket operationId: unsubscribe description: | - ```go - client := client.NewHTTP("tcp:0.0.0.0:26657", "/websocket") - err := client.Start() - if err != nil { - handle error - } - defer client.Stop() - query := "tm.event = 'Tx' AND tx.height = 3" - err = client.Unsubscribe(context.Background(), "test-client", query) - if err != nil { - handle error - } - ``` + ```go + client := client.NewHTTP("tcp:0.0.0.0:26657", "/websocket") + err := client.Start() + if err != nil { + handle error + } + defer client.Stop() + query := "tm.event = 'Tx' AND tx.height = 3" + err = client.Unsubscribe(context.Background(), "test-client", query) + if err != nil { + handle error + } + ``` parameters: - in: query name: query @@ -328,11 +328,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/EmptyResponse' + $ref: "#/definitions/EmptyResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /unsubscribe_all: get: summary: Unsubscribe from all events via WebSocket @@ -347,11 +347,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/EmptyResponse' + $ref: "#/definitions/EmptyResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /health: get: summary: Node heartbeat @@ -366,11 +366,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/EmptyResponse' + $ref: "#/definitions/EmptyResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /status: get: summary: Node Status @@ -385,11 +385,11 @@ paths: 200: description: Status of the node schema: - $ref: '#/definitions/StatusResponse' + $ref: "#/definitions/StatusResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /net_info: get: summary: Network informations @@ -404,11 +404,11 @@ paths: 200: description: empty answer schema: - $ref: '#/definitions/NetInfoResponse' + $ref: "#/definitions/NetInfoResponse" 500: description: empty error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /blockchain: get: summary: Get block headers for minHeight <= height <= maxHeight. @@ -434,11 +434,11 @@ paths: 200: description: Block headers, returned in descending order (highest first). schema: - $ref: '#/definitions/BlockchainResponse' + $ref: "#/definitions/BlockchainResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /block: get: summary: Get block at a specified height @@ -460,11 +460,11 @@ paths: 200: description: Block informations. schema: - $ref: '#/definitions/BlockResponse' + $ref: "#/definitions/BlockResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /block_results: get: summary: Get block results at a specified height @@ -486,11 +486,11 @@ paths: 200: description: Block results. schema: - $ref: '#/definitions/BlockResultsResponse' + $ref: "#/definitions/BlockResultsResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /commit: get: summary: Get commit results at a specified height @@ -512,11 +512,11 @@ paths: 200: description: Commit results. schema: - $ref: '#/definitions/CommitResponse' + $ref: "#/definitions/CommitResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /validators: get: summary: Get validator set at a specified height @@ -538,11 +538,11 @@ paths: 200: description: Commit results. schema: - $ref: '#/definitions/ValidatorsResponse' + $ref: "#/definitions/ValidatorsResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /genesis: get: summary: Get Genesis @@ -557,11 +557,11 @@ paths: 200: description: Genesis results. schema: - $ref: '#/definitions/GenesisResponse' + $ref: "#/definitions/GenesisResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /dump_consensus_state: get: summary: Get consensus state @@ -576,11 +576,11 @@ paths: 200: description: consensus state results. schema: - $ref: '#/definitions/DumpConsensusResponse' + $ref: "#/definitions/DumpConsensusResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /consensus_state: get: summary: Get consensus state @@ -595,11 +595,11 @@ paths: 200: description: consensus state results. schema: - $ref: '#/definitions/ConsensusStateResponse' + $ref: "#/definitions/ConsensusStateResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /consensus_params: get: summary: Get consensus parameters @@ -621,11 +621,11 @@ paths: 200: description: consensus parameters results. schema: - $ref: '#/definitions/ConsensusParamsResponse' + $ref: "#/definitions/ConsensusParamsResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /unconfirmed_txs: get: summary: Get the list of unconfirmed transactions @@ -646,11 +646,11 @@ paths: 200: description: List of unconfirmed transactions schema: - $ref: '#/definitions/UnconfirmedTransactionsResponse' + $ref: "#/definitions/UnconfirmedTransactionsResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /num_unconfirmed_txs: get: summary: Get data about unconfirmed transactions @@ -665,11 +665,11 @@ paths: 200: description: status about unconfirmed transactions schema: - $ref: '#/definitions/NumUnconfirmedTransactionsResponse' + $ref: "#/definitions/NumUnconfirmedTransactionsResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /tx_search: get: summary: Search for transactions @@ -712,11 +712,11 @@ paths: 200: description: List of unconfirmed transactions schema: - $ref: '#/definitions/TxSearchResponse' + $ref: "#/definitions/TxSearchResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /tx: get: summary: Get transactions by hash @@ -745,11 +745,11 @@ paths: 200: description: Get a transaction schema: - $ref: '#/definitions/TxResponse' + $ref: "#/definitions/TxResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /abci_info: get: summary: Get some info about the application. @@ -764,11 +764,11 @@ paths: 200: description: Get some info about the application. schema: - $ref: '#/definitions/ABCIInfoResponse' + $ref: "#/definitions/ABCIInfoResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /abci_query: get: summary: Query the application for some information. @@ -810,11 +810,11 @@ paths: 200: description: Response of the submitted query schema: - $ref: '#/definitions/ABCIQueryResponse' + $ref: "#/definitions/ABCIQueryResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' + $ref: "#/definitions/ErrorResponse" /broadcast_evidence: get: @@ -837,12 +837,11 @@ paths: 200: description: Broadcast evidence of the misbehavior. schema: - $ref: '#/definitions/BroadcastEvidenceResponse' + $ref: "#/definitions/BroadcastEvidenceResponse" 500: description: Error schema: - $ref: '#/definitions/ErrorResponse' - + $ref: "#/definitions/ErrorResponse" definitions: JSONRPC: @@ -857,7 +856,7 @@ definitions: EmptyResponse: description: Empty Response allOf: - - $ref: '#/definitions/JSONRPC' + - $ref: "#/definitions/JSONRPC" - type: object properties: result: @@ -866,7 +865,7 @@ definitions: ErrorResponse: description: Error Response allOf: - - $ref: '#/definitions/JSONRPC' + - $ref: "#/definitions/JSONRPC" - type: object properties: error: @@ -897,7 +896,7 @@ definitions: type: object properties: protocol_version: - $ref: '#/definitions/ProtocolVersion' + $ref: "#/definitions/ProtocolVersion" id: type: string x-example: "5576458aef205977e18fd50b274e9b5d9014525a" @@ -951,7 +950,7 @@ definitions: type: string x-example: "5D6A51A8E9899C44079C6AF90618BA0369070E6E" pub_key: - $ref: '#/definitions/PubKey' + $ref: "#/definitions/PubKey" voting_power: type: string x-example: "0" @@ -960,19 +959,19 @@ definitions: type: object properties: node_info: - $ref: '#/definitions/NodeInfo' + $ref: "#/definitions/NodeInfo" sync_info: - $ref: '#/definitions/SyncInfo' + $ref: "#/definitions/SyncInfo" validator_info: - $ref: '#/definitions/ValidatorInfo' + $ref: "#/definitions/ValidatorInfo" StatusResponse: description: Status Response allOf: - - $ref: '#/definitions/JSONRPC' + - $ref: "#/definitions/JSONRPC" - type: object properties: result: - $ref: '#/definitions/Status' + $ref: "#/definitions/Status" Monitor: type: object properties: @@ -1040,23 +1039,23 @@ definitions: type: string x-example: "168901057956119" SendMonitor: - $ref: '#/definitions/Monitor' + $ref: "#/definitions/Monitor" RecvMonitor: - $ref: '#/definitions/Monitor' + $ref: "#/definitions/Monitor" Channels: type: array items: - $ref: '#/definitions/Channel' + $ref: "#/definitions/Channel" Peer: type: object properties: node_info: - $ref: '#/definitions/NodeInfo' + $ref: "#/definitions/NodeInfo" is_outbound: type: boolean x-example: true connection_status: - $ref: '#/definitions/ConnectionStatus' + $ref: "#/definitions/ConnectionStatus" remote_ip: type: string x-example: "95.179.155.35" @@ -1077,15 +1076,15 @@ definitions: peers: type: array items: - $ref: '#/definitions/Peer' + $ref: "#/definitions/Peer" NetInfoResponse: description: NetInfo Response allOf: - - $ref: '#/definitions/JSONRPC' + - $ref: "#/definitions/JSONRPC" - type: object properties: result: - $ref: '#/definitions/NetInfo' + $ref: "#/definitions/NetInfo" BlockID: required: - "hash" @@ -1154,7 +1153,7 @@ definitions: type: string x-example: "3" last_block_id: - $ref: '#/definitions/BlockID' + $ref: "#/definitions/BlockID" last_commit_hash: type: string x-example: "21B9BC845AD2CB2C4193CDD17BFC506F1EBE5A7402E84AD96E64171287A34812" @@ -1208,9 +1207,9 @@ definitions: type: object properties: block_id: - $ref: '#/definitions/BlockMetaId' + $ref: "#/definitions/BlockMetaId" header: - $ref: '#/definitions/BlockMetaHeader' + $ref: "#/definitions/BlockMetaHeader" Blockchain: type: object required: @@ -1223,15 +1222,15 @@ definitions: block_metas: type: "array" items: - $ref: '#/definitions/BlockMeta' + $ref: "#/definitions/BlockMeta" BlockchainResponse: - description: Blockchain info + description: Blockchain info allOf: - - $ref: '#/definitions/JSONRPC' + - $ref: "#/definitions/JSONRPC" - type: object properties: result: - $ref: '#/definitions/Blockchain' + $ref: "#/definitions/Blockchain" Commit: required: - "type" @@ -1253,7 +1252,7 @@ definitions: type: string x-example: "0" block_id: - $ref: '#/definitions/BlockID' + $ref: "#/definitions/BlockID" timestamp: type: string x-example: "2019-08-01T11:39:38.867269833Z" @@ -1270,30 +1269,30 @@ definitions: type: object properties: header: - $ref: '#/definitions/BlockMetaHeader' + $ref: "#/definitions/BlockMetaHeader" data: type: array items: type: string - x-example: 'yQHwYl3uCkKoo2GaChRnd+THLQ2RM87nEZrE19910Z28ABIUWW/t8AtIMwcyU0sT32RcMDI9GF0aEAoFdWF0b20SBzEwMDAwMDASEwoNCgV1YXRvbRIEMzEwMRCd8gEaagom61rphyEDoJPxlcjRoNDtZ9xMdvs+lRzFaHe2dl2P5R2yVCWrsHISQKkqX5H1zXAIJuC57yw0Yb03Fwy75VRip0ZBtLiYsUqkOsPUoQZAhDNP+6LY+RUwz/nVzedkF0S29NZ32QXdGv0=' + x-example: "yQHwYl3uCkKoo2GaChRnd+THLQ2RM87nEZrE19910Z28ABIUWW/t8AtIMwcyU0sT32RcMDI9GF0aEAoFdWF0b20SBzEwMDAwMDASEwoNCgV1YXRvbRIEMzEwMRCd8gEaagom61rphyEDoJPxlcjRoNDtZ9xMdvs+lRzFaHe2dl2P5R2yVCWrsHISQKkqX5H1zXAIJuC57yw0Yb03Fwy75VRip0ZBtLiYsUqkOsPUoQZAhDNP+6LY+RUwz/nVzedkF0S29NZ32QXdGv0=" evidence: type: array items: - $ref: '#/definitions/Evidence' + $ref: "#/definitions/Evidence" last_commit: type: object properties: block_id: - $ref: '#/definitions/BlockID' + $ref: "#/definitions/BlockID" precommits: type: array items: - $ref: '#/definitions/Commit' + $ref: "#/definitions/Commit" Validator: type: object properties: pub_key: - $ref: '#/definitions/PubKey' + $ref: "#/definitions/PubKey" voting_power: type: number address: @@ -1310,31 +1309,31 @@ definitions: total_voting_power: type: number validator: - $ref: '#/definitions/Validator' + $ref: "#/definitions/Validator" BlockComplete: type: object properties: block_meta: - $ref: '#/definitions/BlockMeta' + $ref: "#/definitions/BlockMeta" block: - $ref: '#/definitions/Block' + $ref: "#/definitions/Block" BlockResponse: - description: Blockc info + description: Blockc info allOf: - - $ref: '#/definitions/JSONRPC' + - $ref: "#/definitions/JSONRPC" - type: object properties: result: - $ref: '#/definitions/BlockComplete' + $ref: "#/definitions/BlockComplete" Tag: type: object properties: key: type: string - example: 'YWN0aW9u' + example: "YWN0aW9u" value: type: string - example: 'c2VuZA==' + example: "c2VuZA==" ################## FROM NOW ON NEEDS REFACTOR ################## BlockResultsResponse: type: "object" @@ -1371,7 +1370,7 @@ definitions: properties: log: type: "string" - example: "[{\"msg_index\":\"0\",\"success\":true,\"log\":\"\"}]" + example: '[{"msg_index":"0","success":true,"log":""}]' gasWanted: type: "string" example: "25629" @@ -2249,7 +2248,7 @@ definitions: - "n_txs" - "total" - "total_bytes" -# - "txs" + # - "txs" properties: n_txs: type: "string" @@ -2260,14 +2259,14 @@ definitions: total_bytes: type: "string" example: "19974" -# txs: -# type: "array" -# x-nullable: true -# items: -# type: "string" -# x-nullable: true -# example: -# - "gAPwYl3uCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUA75/FmYq9WymsOBJ0XSJ8yV8zmQKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhQbrvwbvlNiT+Yjr86G+YQNx7kRVgowjE1xDQoUjJyJG+WaWBwSiGannBRFdrbma+8SFK2m+1oxgILuQLO55n8mWfnbIzyPCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUQNGfkmhTNMis4j+dyMDIWXdIPiYKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhS8sL0D0wwgGCItQwVowak5YB38KRIUCg4KBXVhdG9tEgUxMDA1NBDoxRgaagom61rphyECn8x7emhhKdRCB2io7aS/6Cpuq5NbVqbODmqOT3jWw6kSQKUresk+d+Gw0BhjiggTsu8+1voW+VlDCQ1GRYnMaFOHXhyFv7BCLhFWxLxHSAYT8a5XqoMayosZf9mANKdXArA=" + # txs: + # type: "array" + # x-nullable: true + # items: + # type: "string" + # x-nullable: true + # example: + # - "gAPwYl3uCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUA75/FmYq9WymsOBJ0XSJ8yV8zmQKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhQbrvwbvlNiT+Yjr86G+YQNx7kRVgowjE1xDQoUjJyJG+WaWBwSiGannBRFdrbma+8SFK2m+1oxgILuQLO55n8mWfnbIzyPCjCMTXENChSMnIkb5ZpYHBKIZqecFEV2tuZr7xIUQNGfkmhTNMis4j+dyMDIWXdIPiYKMIxNcQ0KFIyciRvlmlgcEohmp5wURXa25mvvEhS8sL0D0wwgGCItQwVowak5YB38KRIUCg4KBXVhdG9tEgUxMDA1NBDoxRgaagom61rphyECn8x7emhhKdRCB2io7aS/6Cpuq5NbVqbODmqOT3jWw6kSQKUresk+d+Gw0BhjiggTsu8+1voW+VlDCQ1GRYnMaFOHXhyFv7BCLhFWxLxHSAYT8a5XqoMayosZf9mANKdXArA=" type: "object" NumUnconfirmedTransactionsResponse: type: object @@ -2349,7 +2348,7 @@ definitions: properties: log: type: "string" - example: "[{\"msg_index\":\"0\",\"success\":true,\"log\":\"\"}]" + example: '[{"msg_index":"0","success":true,"log":""}]' gasWanted: type: "string" example: "200000" @@ -2450,7 +2449,7 @@ definitions: properties: log: type: "string" - example: "[{\"msg_index\":\"0\",\"success\":true,\"log\":\"\"}]" + example: '[{"msg_index":"0","success":true,"log":""}]' gasWanted: type: "string" example: "200000" @@ -2497,7 +2496,7 @@ definitions: properties: data: type: "string" - example: "{\"size\":0}" + example: '{"size":0}' version: type: string example: "0.16.1" diff --git a/scripts/get_nodejs.sh b/scripts/get_nodejs.sh index 7f0dd6e38..59469cc50 100755 --- a/scripts/get_nodejs.sh +++ b/scripts/get_nodejs.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -VERSION=v11.15.0 +VERSION=v12.9.0 NODE_FULL=node-${VERSION}-linux-x64 mkdir -p ~/.local/bin @@ -10,5 +10,5 @@ tar -xzf ~/.local/node/${NODE_FULL}.tar.gz -C ~/.local/node/ ln -s ~/.local/node/${NODE_FULL}/bin/node ~/.local/bin/node ln -s ~/.local/node/${NODE_FULL}/bin/npm ~/.local/bin/npm export PATH=~/.local/bin:$PATH -npm i -g dredd@11.0.1 +npm i -g dredd ln -s ~/.local/node/${NODE_FULL}/bin/dredd ~/.local/bin/dredd From 897983d54dd59fbd53f092e6b5c1d30d797b3de0 Mon Sep 17 00:00:00 2001 From: josef-widder <44643235+josef-widder@users.noreply.github.com> Date: Tue, 27 Aug 2019 15:18:01 +0200 Subject: [PATCH 184/211] Updated lite client spec (#3841) * updates lite cline spec based on Zarko's and my previous specs * updates spec incorporating comments from Aug 8 meeting * added check of validators * Update docs/spec/consensus/light-client.md Co-Authored-By: Anca Zamfir * Update docs/spec/consensus/light-client.md Co-Authored-By: Anca Zamfir * Update docs/spec/consensus/light-client.md Co-Authored-By: Anca Zamfir * Update docs/spec/consensus/light-client.md Co-Authored-By: Anca Zamfir * fix typos & improve formatting Co-Authored-By: Anca Zamfir * Update docs/spec/consensus/light-client.md Co-Authored-By: Anton Kaliaev * incorporated some of Anca's and Anton's comments * addressed all current comments * highlight assumptions/verification conditions * added remark on checks * Update docs/spec/consensus/light-client.md Co-Authored-By: Ethan Buchman * Update docs/spec/consensus/light-client.md Co-Authored-By: Ethan Buchman * Update docs/spec/consensus/light-client.md Co-Authored-By: Ethan Buchman * fixes * addressed Ethan's comments from Aug 18 * trustlevel * description of 3 docs fixed * added comment on justification asked for by Anca * bla * removed trustlevel for adjacent headers. Added bisection order. * added comment in bisection * fixed comment in checksupport * added comment on bisection timeout --- docs/spec/consensus/light-client.md | 422 +++++++++++++++++++++------- 1 file changed, 319 insertions(+), 103 deletions(-) diff --git a/docs/spec/consensus/light-client.md b/docs/spec/consensus/light-client.md index 4b683b9a6..18dc280a3 100644 --- a/docs/spec/consensus/light-client.md +++ b/docs/spec/consensus/light-client.md @@ -1,113 +1,329 @@ -# Light Client - -A light client is a process that connects to the Tendermint Full Node(s) and then tries to verify the Merkle proofs -about the blockchain application. In this document we describe mechanisms that ensures that the Tendermint light client -has the same level of security as Full Node processes (without being itself a Full Node). - -To be able to validate a Merkle proof, a light client needs to validate the blockchain header that contains the root app hash. -Validating a blockchain header in Tendermint consists in verifying that the header is committed (signed) by >2/3 of the -voting power of the corresponding validator set. As the validator set is a dynamic set (it is changing), one of the -core functionality of the light client is updating the current validator set, that is then used to verify the -blockchain header, and further the corresponding Merkle proofs. - -For the purpose of this light client specification, we assume that the Tendermint Full Node exposes the following functions over -Tendermint RPC: - -```golang -Header(height int64) (SignedHeader, error) // returns signed header for the given height -Validators(height int64) (ResultValidators, error) // returns validator set for the given height -LastHeader(valSetNumber int64) (SignedHeader, error) // returns last header signed by the validator set with the given validator set number - -type SignedHeader struct { - Header Header - Commit Commit - ValSetNumber int64 -} +# Lite client -type ResultValidators struct { - BlockHeight int64 - Validators []Validator - // time the current validator set is initialised, i.e, time of the last validator change before header BlockHeight - ValSetTime int64 -} +A lite client is a process that connects to Tendermint full nodes and then tries to verify application data using the Merkle proofs. + +## Context of this document + +In order to make sure that full nodes have the incentive to follow the protocol, we have to address the following three Issues + +1) The lite client needs a method to verify headers it obtains from full nodes according to trust assumptions -- this document. + +2) The lite client must be able to connect to one correct full node to detect and report on failures in the trust assumptions (i.e., conflicting headers) -- a future document. + +3) In the event the trust assumption fails (i.e., a lite client is fooled by a conflicting header), the Tendermint fork accountability protocol must account for the evidence -- see #3840 + +## Problem statement + + +We assume that the lite client knows a (base) header *inithead* it trusts (by social consensus or because the lite client has decided to trust the header before). The goal is to check whether another header *newhead* can be trusted based on the data in *inithead*. + +The correctness of the protocol is based on the assumption that *inithead* was generated by an instance of Tendermint consensus. The term "trusting" above indicates that the correctness on the protocol depends on this assumption. It is in the responsibility of the user that runs the lite client to make sure that the risk of trusting a corrupted/forged *inithead* is negligible. + + +## Definitions + +### Data structures + +In the following, only the details of the data structures needed for this specification are given. + + * header fields + - *height* + - *bfttime*: the chain time when the header (block) was generated + - *V*: validator set containing validators for this block. + - *NextV*: validator set for next block. + - *commit*: evidence that block with height *height* - 1 was committed by a set of validators (canonical commit). We will use ```signers(commit)``` to refer to the set of validators that committed the block. + + * signed header fields: contains a header and a *commit* for the current header; a "seen commit". In the Tendermint consensus the "canonical commit" is stored in header *height* + 1. + + * For each header *h* it has locally stored, the lite client stores whether + it trusts *h*. We write *trust(h) = true*, if this is the case. + + * Validator fields. We will write a validator as a tuple *(v,p)* such that + + *v* is the identifier (we assume identifiers are unique in each validator set) + + *p* is its voting power + + +### Functions + +For the purpose of this lite client specification, we assume that the Tendermint Full Node exposes the following function over Tendermint RPC: +```go + func Commit(height int64) (SignedHeader, error) + // returns signed header: header (with the fields from + // above) with Commit that include signatures of + // validators that signed the header + + + type SignedHeader struct { + Header Header + Commit Commit + } ``` -We assume that Tendermint keeps track of the validator set changes and that each time a validator set is changed it is -being assigned the next sequence number. We can call this number the validator set sequence number. Tendermint also remembers -the Time from the header when the next validator set is initialised (starts to be in power), and we refer to this time -as validator set init time. -Furthermore, we assume that each validator set change is signed (committed) by the current validator set. More precisely, -given a block `H` that contains transactions that are modifying the current validator set, the Merkle root hash of the next -validator set (modified based on transactions from block H) will be in block `H+1` (and signed by the current validator -set), and then starting from the block `H+2`, it will be signed by the next validator set. - -Note that the real Tendermint RPC API is slightly different (for example, response messages contain more data and function -names are slightly different); we shortened (and modified) it for the purpose of this document to make the spec more -clear and simple. Furthermore, note that in case of the third function, the returned header has `ValSetNumber` equals to -`valSetNumber+1`. - -Locally, light client manages the following state: - -```golang -valSet []Validator // current validator set (last known and verified validator set) -valSetNumber int64 // sequence number of the current validator set -valSetHash []byte // hash of the current validator set -valSetTime int64 // time when the current validator set is initialised +### Definitions + +* *tp*: trusting period +* for realtime *t*, the predicate *correct(v,t)* is true if the validator *v* + follows the protocol until time *t* (we will see about recovery later). + + + + +### Tendermint Failure Model + +If a block *h* is generated at time *bfttime* (and this time is stored in the block), then a set of validators that hold more than 2/3 of the voting power in h.Header.NextV is correct until time h.Header.bfttime + tp. + +Formally, +\[ +\sum_{(v,p) \in h.Header.NextV \wedge correct(v,h.Header.bfttime + tp)} p > +2/3 \sum_{(v,p) \in h.Header.NextV} p +\] + +*Assumption*: "correct" is defined w.r.t. realtime (some Newtonian global notion of time, i.e., wall time), while *bfttime* corresponds to the reading of the local clock of a validator (how this time is computed may change when the Tendermint consensus is modified). In this note, we assume that all clocks are synchronized to realtime. We can make this more precise eventually (incorporating clock drift, accuracy, precision, etc.). Right now, we consider this assumption sufficient, as clock synchronization (under NTP) is in the order of milliseconds and *tp* is in the order of weeks. + +*Remark*: This failure model might change to a hybrid version that takes heights into account in the future. + +The specification in this document considers an implementation of the lite client under this assumption. Issues like *counter-factual signing* and *fork accountability* and *evidence submission* are mechanisms that justify this assumption by incentivizing validators to follow the protocol. +If they don't, and we have more that 1/3 faults, safety may be violated. Our approach then is to *detect* these cases (after the fact), and take suitable repair actions (automatic and social). This is discussed in an upcoming document on "Fork accountability". (These safety violations include the lite client wrongly trusting a header, a fork in the blockchain, etc.) + + +## Lite Client Trusting Spec + +The lite client communicates with a full node and learns new headers. The goal is to locally decide whether to trust a header. Our implementation needs to ensure the following two properties: + +- Lite Client Completeness: If header *h* was correctly generated by an instance of Tendermint consensus (and its age is less than the trusting period), then the lite client should eventually set *trust(h)* to true. + +- Lite Client Accuracy: If header *h* was *not generated* by an instance of Tendermint consensus, then the lite client should never set *trust(h)* to true. + +*Remark*: If in the course of the computation, the lite client obtains certainty that some headers were forged by adversaries (that is were not generated by an instance of Tendermint consensus), it may submit (a subset of) the headers it has seen as evidence of misbehavior. + +*Remark*: In Completeness we use "eventually", while in practice *trust(h)* should be set to true before *h.Header.bfttime + tp*. If not, the block cannot be trusted because it is too old. + +*Remark*: If a header *h* is marked with *trust(h)*, but it is too old (its bfttime is more than *tp* ago), then the lite client should set *trust(h)* to false again. + +*Assumption*: Initially, the lite client has a header *inithead* that it trusts correctly, that is, *inithead* was correctly generated by the Tendermint consensus. + +To reason about the correctness, we may prove the following invariant. + +*Verification Condition: Lite Client Invariant.* + For each lite client *l* and each header *h*: +if *l* has set *trust(h) = true*, + then validators that are correct until time *h.Header.bfttime + tp* have more than two thirds of the voting power in *h.Header.NextV*. + + Formally, + \[ + \sum_{(v,p) \in h.Header.NextV \wedge correct(v,h.Header.bfttime + tp)} p > + 2/3 \sum_{(v,p) \in h.Header.NextV} p + \] + +*Remark.* To prove the invariant, we will have to prove that the lite client only trusts headers that were correctly generated by Tendermint consensus, then the formula above follows from the Tendermint failure model. + + +## High Level Solution + +Upon initialization, the lite client is given a header *inithead* it trusts (by +social consensus). It is assumed that *inithead* satisfies the lite client invariant. (If *inithead* has been correctly generated by Tendermint consensus, the invariant follows from the Tendermint Failure Model.) + +When a lite clients sees a signed new header *snh*, it has to decide whether to trust the new +header. Trust can be obtained by (possibly) the combination of three methods. + +1. **Uninterrupted sequence of proof.** If a block is appended to the chain, where the last block +is trusted (and properly committed by the old validator set in the next block), +and the new block contains a new validator set, the new block is trusted if the lite client knows all headers in the prefix. +Intuitively, a trusted validator set is assumed to only chose a new validator set that will obey the Tendermint Failure Model. + +2. **Trusting period.** Based on a trusted block *h*, and the lite client +invariant, which ensures the fault assumption during the trusting period, we can check whether at least one validator, that has been continuously correct from *h.Header.bfttime* until now, has signed *snh*. +If this is the case, similarly to above, the chosen validator set in *snh* does not violate the Tendermint Failure Model. + +3. **Bisection.** If a check according to the trusting period fails, the lite client can try to obtain a header *hp* whose height lies between *h* and *snh* in order to check whether *h* can be used to get trust for *hp*, and *hp* can be used to get trust for *snh*. If this is the case we can trust *snh*; if not, we may continue recursively. + +## How to use it + +We consider the following use case: + the lite client wants to verify a header for some given height *k*. Thus: + - it requests the signed header for height *k* from a full node + - it tries to verify this header with the methods described here. + +This can be used in several settings: + - someone tells the lite client that application data that is relevant for it can be read in the block of height *k*. + - the lite clients wants the latest state. It asks a full nude for the current height, and uses the response for *k*. + + +## Details + +*Assumptions* + +1. *tp < unbonding period*. +2. *snh.Header.bfttime < now* +3. *snh.Header.bfttime < h.Header.bfttime+tp* +4. *trust(h)=true* + + +**Observation 1.** If *h.Header.bfttime + tp > now*, we trust the old +validator set *h.Header.NextV*. + +When we say we trust *h.Header.NextV* we do *not* trust that each individual validator in *h.Header.NextV* is correct, but we only trust the fact that at most 1/3 of them are faulty (more precisely, the faulty ones have at most 1/3 of the total voting power). + + + +### Functions + +The function *Bisection* checks whether to trust header *h2* based on the trusted header *h1*. It does so by calling +the function *CheckSupport* in the process of +bisection/recursion. *CheckSupport* implements the trusted period method and, for two adjacent headers (in term of heights), it checks uninterrupted sequence of proof. + +*Assumption*: In the following, we assume that *h2.Header.height > h1.Header.height*. We will quickly discuss the other case in the next section. + +We consider the following set-up: +- the lite client communicates with one full node +- the lite client locally stores all the signed headers it obtained (trusted or not). In the pseudo code below we write *Store(header)* for this. +- If *Bisection* returns *false*, then the lite client has seen a forged header. + * However, it does not know which header(s) is/are the problematic one(s). + * In this case, the lite client can submit (some of) the headers it has seen as evidence. As the lite client communicates with one full node only when executing Bisection, there are two cases + - the full node is faulty + - the full node is correct and there was a fork in Tendermint consensus. Header *h1* is from a different branch than the one taken by the full node. This case is not focus of this document, but will be treated in the document on fork accountability. + +- the lite client must retry to retrieve correct headers from another full node + * it picks a new full node + * it restarts *Bisection* + * there might be optimizations; a lite client may not need to call *Commit(k)*, for a height *k* for which it already has a signed header it trusts. + * how to make sure that a lite client can communicate with a correct full node will be the focus of a separate document (recall Issue 3 from "Context of this document"). + +**Auxiliary Functions.** We will use the function ```votingpower_in(V1,V2)``` to compute the voting power the validators in set V1 have according to their voting power in set V2; +we will write ```totalVotingPower(V)``` for ```votingpower_in(V,V)```, which returns the total voting power in V. +We further use the function ```signers(Commit)``` that returns the set of validators that signed the Commit. + +**CheckSupport.** The following function checks whether we can trust the header h2 based on header h1 following the trusting period method. + +```go + func CheckSupport(h1,h2,trustlevel) bool { + if h1.Header.bfttime + tp < now { // Observation 1 + return false // old header was once trusted but it is expired + } + vp_all := totalVotingPower(h1.Header.NextV) + // total sum of voting power of validators in h2 + + if h2.Header.height == h1.Header.height + 1 { + // specific check for adjacent headers; everything must be + // properly signed. + // also check that h2.Header.V == h1.Header.NextV + // Plus the following check that 2/3 of the voting power + // in h1 signed h2 + return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > + 2/3 * vp_all) + // signing validators are more than two third in h1. + } + + return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > + max(1/3,trustlevel) * vp_all) + // get validators in h1 that signed h2 + // sum of voting powers in h1 of + // validators that signed h2 + // is more than a third in h1 + } ``` -The light client is initialised with the trusted validator set, for example based on the known validator set hash, -validator set sequence number and the validator set init time. -The core of the light client logic is captured by the VerifyAndUpdate function that is used to 1) verify if the given header is valid, -and 2) update the validator set (when the given header is valid and it is more recent than the seen headers). + *Remark*: Basic header verification must be done for *h2*. Similar checks are done in: + https://github.com/tendermint/tendermint/blob/master/types/validator_set.go#L591-L633 + + *Remark*: There are some sanity checks which are not in the code: + *h2.Header.height > h1.Header.height* and *h2.Header.bfttime > h1.Header.bfttime* and *h2.Header.bfttime < now*. + + *Remark*: ```return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > max(1/3,trustlevel) * vp_all)``` may return false even if *h2* was properly generated by Tendermint consensus in the case of big changes in the validator sets. However, the check ```return (votingpower_in(signers(h2.Commit),h1.Header.NextV) > + 2/3 * vp_all)``` must return true if *h1* and *h2* were generated by Tendermint consensus. + +*Remark*: The 1/3 check differs from a previously proposed method that was based on intersecting validator sets and checking that the new validator set contains "enough" correct validators. We found that the old check is not suited for realistic changes in the validator sets. The new method is not only based on cardinalities, but also exploits that we can trust what is signed by a correct validator (i.e., signed by more than 1/3 of the voting power). -```golang -VerifyAndUpdate(signedHeader SignedHeader): - assertThat signedHeader.valSetNumber >= valSetNumber - if isValid(signedHeader) and signedHeader.Header.Time <= valSetTime + UNBONDING_PERIOD then - setValidatorSet(signedHeader) +*Correctness arguments* + +Towards Lite Client Accuracy: +- Assume by contradiction that *h2* was not generated correctly and the lite client sets trust to true because *CheckSupport* returns true. +- h1 is trusted and sufficiently new +- by Tendermint Fault Model, less than 1/3 of voting power held by faulty validators => at least one correct validator *v* has signed *h2*. +- as *v* is correct up to now, it followed the Tendermint consensus protocol at least up to signing *h2* => *h2* was correctly generated, we arrive at the required contradiction. + + +Towards Lite Client Completeness: +- The check is successful if sufficiently many validators of *h1* are still validators in *h2* and signed *h2*. +- If *h2.Header.height = h1.Header.height + 1*, and both headers were generated correctly, the test passes + +*Verification Condition:* We may need a Tendermint invariant stating that if *h2.Header.height = h1.Header.height + 1* then *signers(h2.Commit) \subseteq h1.Header.NextV*. + +*Remark*: The variable *trustlevel* can be used if the user believes that relying on one correct validator is not sufficient. However, in case of (frequent) changes in the validator set, the higher the *trustlevel* is chosen, the more unlikely it becomes that CheckSupport returns true for non-adjacent headers. + +**Bisection.** The following function uses CheckSupport in a recursion to find intermediate headers that allow to establish a sequence of trust. + + + + +```go +func Bisection(h1,h2,trustlevel) bool{ + if CheckSupport(h1,h2,trustlevel) { return true - else - updateValidatorSet(signedHeader.ValSetNumber) - return VerifyAndUpdate(signedHeader) - -isValid(signedHeader SignedHeader): - valSetOfTheHeader = Validators(signedHeader.Header.Height) - assertThat Hash(valSetOfTheHeader) == signedHeader.Header.ValSetHash - assertThat signedHeader is passing basic validation - if votingPower(signedHeader.Commit) > 2/3 * votingPower(valSetOfTheHeader) then return true - else + } + if h2.Header.height == h1.Header.height + 1 { + // we have adjacent headers that are not matching (failed + // the CheckSupport) + // we could submit evidence here return false + } + pivot := (h1.Header.height + h2.Header.height) / 2 + hp := Commit(pivot) + // ask a full node for header of height pivot + Store(hp) + // store header hp locally + if Bisection(h1,hp,trustlevel) { + // only check right branch if hp is trusted + // (otherwise a lot of unnecessary computation may be done) + return Bisection(hp,h2,trustlevel) + } + else { + return false + } +} +``` -setValidatorSet(signedHeader SignedHeader): - nextValSet = Validators(signedHeader.Header.Height) - assertThat Hash(nextValSet) == signedHeader.Header.ValidatorsHash - valSet = nextValSet.Validators - valSetHash = signedHeader.Header.ValidatorsHash - valSetNumber = signedHeader.ValSetNumber - valSetTime = nextValSet.ValSetTime - -votingPower(commit Commit): - votingPower = 0 - for each precommit in commit.Precommits do: - if precommit.ValidatorAddress is in valSet and signature of the precommit verifies then - votingPower += valSet[precommit.ValidatorAddress].VotingPower - return votingPower - -votingPower(validatorSet []Validator): - for each validator in validatorSet do: - votingPower += validator.VotingPower - return votingPower - -updateValidatorSet(valSetNumberOfTheHeader): - while valSetNumber != valSetNumberOfTheHeader do - signedHeader = LastHeader(valSetNumber) - if isValid(signedHeader) then - setValidatorSet(signedHeader) - else return error - return -``` -Note that in the logic above we assume that the light client will always go upward with respect to header verifications, -i.e., that it will always be used to verify more recent headers. In case a light client needs to be used to verify older -headers (go backward) the same mechanisms and similar logic can be used. In case a call to the FullNode or subsequent -checks fail, a light client need to implement some recovery strategy, for example connecting to other FullNode. + + +*Correctness arguments (sketch)* + +Lite Client Accuracy: +- Assume by contradiction that *h2* was not generated correctly and the lite client sets trust to true because Bisection returns true. +- Bisection returns true only if all calls to CheckSupport in the recursion return true. +- Thus we have a sequence of headers that all satisfied the CheckSupport +- again a contradiction + +Lite Client Completeness: + +This is only ensured if upon *Commit(pivot)* the lite client is always provided with a correctly generated header. + +*Stalling* + +With Bisection, a faulty full node could stall a lite client by creating a long sequence of headers that are queried one-by-one by the lite client and look OK, before the lite client eventually detects a problem. There are several ways to address this: +* Each call to ```Commit``` could be issued to a different full node +* Instead of querying header by header, the lite client tells a full node which header it trusts, and the height of the header it needs. The full node responds with the header along with a proof consisting of intermediate headers that the light client can use to verify. Roughly, Bisection would then be executed at the full node. +* We may set a timeout how long bisection may take. + + +### The case *h2.Header.height < h1.Header.height* + +In the use case where someone tells the lite client that application data that is relevant for it can be read in the block of height *k* and the lite client trusts a more recent header, we can use the hashes to verify headers "down the chain." That is, we iterate down the heights and check the hashes in each step. + +*Remark.* For the case were the lite client trusts two headers *i* and *j* with *i < k < j*, we should discuss/experiment whether the forward or the backward method is more effective. + +```go +func Backwards(h1,h2) bool { + assert (h2.Header.height < h1.Header.height) + old := h1 + for i := h1.Header.height - 1; i > h2.Header.height; i-- { + new := Commit(i) + Store(new) + if (hash(new) != old.Header.hash) { + return false + } + old := new + } + return (hash(h2) == old.Header.hash) + } +``` From 5ac44cfa0fc02925c68470ae6913a7f3b4b131ab Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Wed, 28 Aug 2019 12:00:55 +0400 Subject: [PATCH 185/211] v0.32.3 changelog and version bump (#3924) --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ CHANGELOG_PENDING.md | 9 --------- version/version.go | 2 +- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ba24a6dd..7644c0fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ # Changelog +## v0.32.3 + +*August 28, 2019* + +@climber73 wrote the [Writing a Tendermint Core application in Java +(gRPC)](https://github.com/tendermint/tendermint/blob/master/docs/guides/java.md) +guide. + +Special thanks to external contributors on this release: +@gchaincl, @bluele, @climber73 + +Friendly reminder, we have a [bug bounty +program](https://hackerone.com/tendermint). + +### IMPROVEMENTS: + +- [consensus] [\#3839](https://github.com/tendermint/tendermint/issues/3839) Reduce "Error attempting to add vote" message severity (Error -> Info) +- [mempool] [\#3877](https://github.com/tendermint/tendermint/pull/3877) Make `max_tx_bytes` configurable instead of `max_msg_bytes` (@bluele) +- [privval] [\#3370](https://github.com/tendermint/tendermint/issues/3370) Refactor and simplify validator/kms connection handling. Please refer to [this comment](https://github.com/tendermint/tendermint/pull/3370#issue-257360971) for details +- [rpc] [\#3880](https://github.com/tendermint/tendermint/issues/3880) Document endpoints with `swagger`, introduce contract tests of implementation against documentation + +### BUG FIXES: + +- [config] [\#3868](https://github.com/tendermint/tendermint/issues/3868) Move misplaced `max_msg_bytes` into mempool section (@bluele) +- [rpc] [\#3910](https://github.com/tendermint/tendermint/pull/3910) Fix DATA RACE in HTTP client (@gchaincl) +- [store] [\#3893](https://github.com/tendermint/tendermint/issues/3893) Fix "Unregistered interface types.Evidence" panic + ## v0.32.2 *July 31, 2019* diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 2912b63b6..9d659f84f 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -13,13 +13,4 @@ program](https://hackerone.com/tendermint). ### IMPROVEMENTS: -- [privval] \#3370 Refactors and simplifies validator/kms connection handling. Please refer to thttps://github.com/tendermint/tendermint/pull/3370#issue-257360971 -- [consensus] \#3839 Reduce "Error attempting to add vote" message severity (Error -> Info) -- [mempool] \#3877 Make `max_tx_bytes` configurable instead of `max_msg_bytes` -- [rpc] \#3880 Document endpoints with `swagger`, introduce contract tests of implementation against documentation - ### BUG FIXES: - -- [config] \#3868 move misplaced `max_msg_bytes` into mempool section -- [store] \#3893 register block amino, not just crypto -- [rpc] [\#3910](https://github.com/tendermint/tendermint/pull/3910) protect subscription access from race conditions (@gchaincl) diff --git a/version/version.go b/version/version.go index 9fb7c7869..e1c01c4c2 100644 --- a/version/version.go +++ b/version/version.go @@ -20,7 +20,7 @@ const ( // Must be a string because scripts like dist.sh read this file. // XXX: Don't change the name of this variable or you will break // automation :) - TMCoreSemVer = "0.32.2" + TMCoreSemVer = "0.32.3" // ABCISemVer is the semantic version of the ABCI library ABCISemVer = "0.16.1" From 4a6542984a0459698ab0f3844a5060c39682f34d Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Wed, 11 Sep 2019 11:24:46 +0800 Subject: [PATCH 186/211] fix failed unit tests --- blockchain/hot/pool_test.go | 17 ++++++++++------- blockchain/hot/reactor.go | 2 +- blockchain/hot/reactor_test.go | 10 +++++++--- blockchain/v1/pool_test.go | 2 +- blockchain/v1/reactor_fsm_test.go | 2 +- blockchain/v1/reactor_test.go | 4 ++-- consensus/replay_test.go | 4 ++-- consensus/state.go | 10 +++++----- mempool/clist_mempool_test.go | 12 ++++++------ rpc/test/helpers.go | 6 +++--- state/state_test.go | 6 +++--- state/txindex/kv/kv_bench_test.go | 3 +-- state/txindex/kv/kv_test.go | 2 +- types/vote_test.go | 4 ++-- 14 files changed, 45 insertions(+), 39 deletions(-) diff --git a/blockchain/hot/pool_test.go b/blockchain/hot/pool_test.go index 29d3e6593..a74e96bce 100644 --- a/blockchain/hot/pool_test.go +++ b/blockchain/hot/pool_test.go @@ -1,7 +1,6 @@ package hot import ( - "github.com/tendermint/tendermint/store" "os" "sort" "testing" @@ -15,9 +14,11 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + tmmock "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/p2p/mock" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" tmtime "github.com/tendermint/tendermint/types/time" ) @@ -513,8 +514,10 @@ func newBlockchainPoolPair(logger log.Logger, genDoc *types.GenesisDoc, privVals // Make the BlockPool itself. // NOTE we have to create and commit the blocks first because // pool.height is determined from the store. - blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), proxyApp.Consensus(), - sm.MockMempool{}, sm.MockEvidencePool{}, true) + db := dbm.NewMemDB() + blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), + tmmock.Mempool{}, sm.MockEvidencePool{}, true) + sm.SaveState(db, state) // let's add some blocks in for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { lastCommit := types.NewCommit(types.BlockID{}, nil) @@ -552,7 +555,7 @@ type BlockPoolPair struct { privVals []types.PrivValidator } -func nextBlock(state sm.State, blockStore *blockchain.BlockStore, blockExec *sm.BlockExecutor, pri types.PrivValidator) (*types.Block, *types.Commit, *types.VoteSet) { +func nextBlock(state sm.State, blockStore *store.BlockStore, blockExec *sm.BlockExecutor, pri types.PrivValidator) (*types.Block, *types.Commit, *types.VoteSet) { height := blockStore.Height() lastBlockMeta := blockStore.LoadBlockMeta(height) lastBlock := blockStore.LoadBlock(height) @@ -587,11 +590,11 @@ func (app *testApp) EndBlock(req abci.RequestEndBlock) abci.ResponseEndBlock { return abci.ResponseEndBlock{} } -func (app *testApp) DeliverTx(tx []byte) abci.ResponseDeliverTx { - return abci.ResponseDeliverTx{Tags: []cmn.KVPair{}} +func (app *testApp) DeliverTx(req abci.RequestDeliverTx) abci.ResponseDeliverTx { + return abci.ResponseDeliverTx{} } -func (app *testApp) CheckTx(tx []byte) abci.ResponseCheckTx { +func (app *testApp) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { return abci.ResponseCheckTx{} } diff --git a/blockchain/hot/reactor.go b/blockchain/hot/reactor.go index b79f04872..2013a7c2e 100644 --- a/blockchain/hot/reactor.go +++ b/blockchain/hot/reactor.go @@ -197,7 +197,7 @@ func (hbcR *BlockchainReactor) switchRoutine() { case <-hbcR.pool.Quit(): return case <-switchToConsensusTicker.C: - if hbcR.pool.getSyncPattern() == Hot && hbcR.privValidator != nil && hbcR.pool.state.Validators.HasAddress(hbcR.privValidator.GetAddress()) { + if hbcR.pool.getSyncPattern() == Hot && hbcR.privValidator != nil && hbcR.pool.state.Validators.HasAddress(hbcR.privValidator.GetPubKey().Address()) { hbcR.Logger.Info("hot sync switching to consensus sync") conR, ok := hbcR.Switch.Reactor("CONSENSUS").(consensusReactor) if ok { diff --git a/blockchain/hot/reactor_test.go b/blockchain/hot/reactor_test.go index 119208733..c0321e206 100644 --- a/blockchain/hot/reactor_test.go +++ b/blockchain/hot/reactor_test.go @@ -11,9 +11,11 @@ import ( cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" + "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" sm "github.com/tendermint/tendermint/state" + "github.com/tendermint/tendermint/store" "github.com/tendermint/tendermint/types" ) @@ -220,7 +222,7 @@ func newBlockchainReactorPair(logger log.Logger, genDoc *types.GenesisDoc, privV } blockDB := dbm.NewMemDB() stateDB := dbm.NewMemDB() - blockStore := blockchain.NewBlockStore(blockDB) + blockStore := store.NewBlockStore(blockDB) state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) if err != nil { panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) @@ -229,8 +231,10 @@ func newBlockchainReactorPair(logger log.Logger, genDoc *types.GenesisDoc, privV // Make the BlockPool itself. // NOTE we have to create and commit the blocks first because // pool.height is determined from the store. - blockExec := sm.NewBlockExecutor(dbm.NewMemDB(), log.TestingLogger(), proxyApp.Consensus(), - sm.MockMempool{}, sm.MockEvidencePool{}, true) + db := dbm.NewMemDB() + blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), + mock.Mempool{}, sm.MockEvidencePool{}, true) + sm.SaveState(db, state) // let's add some blocks in for blockHeight := int64(1); blockHeight <= maxBlockHeight; blockHeight++ { lastCommit := types.NewCommit(types.BlockID{}, nil) diff --git a/blockchain/v1/pool_test.go b/blockchain/v1/pool_test.go index 72758d3b1..437fc7e05 100644 --- a/blockchain/v1/pool_test.go +++ b/blockchain/v1/pool_test.go @@ -44,7 +44,7 @@ func (testR *testBcR) sendBlockRequest(peerID p2p.ID, height int64) error { func (testR *testBcR) resetStateTimer(name string, timer **time.Timer, timeout time.Duration) { } -func (testR *testBcR) switchToConsensus() { +func (testR *testBcR) switchToConsensusOrHotSync() { } diff --git a/blockchain/v1/reactor_fsm_test.go b/blockchain/v1/reactor_fsm_test.go index 54e177f25..7caeedd84 100644 --- a/blockchain/v1/reactor_fsm_test.go +++ b/blockchain/v1/reactor_fsm_test.go @@ -932,7 +932,7 @@ func (testR *testReactor) resetStateTimer(name string, timer **time.Timer, timeo } } -func (testR *testReactor) switchToConsensus() { +func (testR *testReactor) switchToConsensusOrHotSync() { } // ---------------------------------------- diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go index 5f01c852a..a7e47b4c4 100644 --- a/blockchain/v1/reactor_test.go +++ b/blockchain/v1/reactor_test.go @@ -96,7 +96,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals fastSync := true db := dbm.NewMemDB() blockExec := sm.NewBlockExecutor(db, log.TestingLogger(), proxyApp.Consensus(), - mock.Mempool{}, sm.MockEvidencePool{}) + mock.Mempool{}, sm.MockEvidencePool{}, true) sm.SaveState(db, state) // let's add some blocks in @@ -123,7 +123,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals blockStore.SaveBlock(thisBlock, thisParts, lastCommit) } - bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync) + bcReactor := NewBlockchainReactor(state.Copy(), blockExec, blockStore, fastSync, config.HotSyncReactor, config.HotSync) bcReactor.SetLogger(logger.With("module", "blockchain")) return bcReactor diff --git a/consensus/replay_test.go b/consensus/replay_test.go index 3762f8816..0f21a12a0 100644 --- a/consensus/replay_test.go +++ b/consensus/replay_test.go @@ -796,7 +796,7 @@ func TestHandshakePanicsIfAppReturnsWrongAppHash(t *testing.T) { defer proxyApp.Stop() assert.Panics(t, func() { - h := NewHandshaker(stateDB, state, store, genDoc) + h := NewHandshaker(stateDB, state, store, genDoc, true) h.Handshake(proxyApp) }) } @@ -814,7 +814,7 @@ func TestHandshakePanicsIfAppReturnsWrongAppHash(t *testing.T) { defer proxyApp.Stop() assert.Panics(t, func() { - h := NewHandshaker(stateDB, state, store, genDoc) + h := NewHandshaker(stateDB, state, store, genDoc, true) h.Handshake(proxyApp) }) } diff --git a/consensus/state.go b/consensus/state.go index 85b45b0ea..8a46f535d 100644 --- a/consensus/state.go +++ b/consensus/state.go @@ -873,7 +873,7 @@ func (cs *ConsensusState) enterPropose(height int64, round int) { } // if not a validator, we're done - address := cs.privValidator.GetAddress() + address := cs.privValidator.GetPubKey().Address() if !cs.Validators.HasAddress(address) { logger.Debug("This node is not a validator", "addr", address, "vals", cs.Validators) return @@ -966,7 +966,7 @@ func (cs *ConsensusState) createProposalBlock() (block *types.Block, blockParts return } - proposerAddr := cs.privValidator.GetAddress() + proposerAddr := cs.privValidator.GetPubKey().Address() return cs.blockExec.CreateProposalBlock(cs.Height, cs.state, commit, proposerAddr) } @@ -1510,7 +1510,7 @@ func (cs *ConsensusState) tryAddVote(vote *types.Vote, peerID p2p.ID) (bool, err if err == ErrVoteHeightMismatch { return added, err } else if voteErr, ok := err.(*types.ErrVoteConflictingVotes); ok { - addr := cs.privValidator.GetAddress() + addr := cs.privValidator.GetPubKey().Address() if bytes.Equal(vote.ValidatorAddress, addr) { cs.Logger.Error("Found conflicting vote from ourselves. Did you unsafe_reset a validator?", "height", vote.Height, "round", vote.Round, "type", vote.Type) return added, err @@ -1688,7 +1688,7 @@ func (cs *ConsensusState) signVote(type_ types.SignedMsgType, hash []byte, heade // Flush the WAL. Otherwise, we may not recompute the same vote to sign, and the privValidator will refuse to sign anything. cs.wal.FlushAndSync() - addr := cs.privValidator.GetAddress() + addr := cs.privValidator.GetPubKey().Address() valIndex, _ := cs.Validators.GetByAddress(addr) vote := &types.Vote{ @@ -1724,7 +1724,7 @@ func (cs *ConsensusState) voteTime() time.Time { // sign the vote and publish on internalMsgQueue func (cs *ConsensusState) signAddVote(type_ types.SignedMsgType, hash []byte, header types.PartSetHeader) *types.Vote { // if we don't have a key or we're not in the validator set, do nothing - if cs.privValidator == nil || !cs.Validators.HasAddress(cs.privValidator.GetAddress()) { + if cs.privValidator == nil || !cs.Validators.HasAddress(cs.privValidator.GetPubKey().Address()) { return nil } vote, err := cs.signVote(type_, hash, header) diff --git a/mempool/clist_mempool_test.go b/mempool/clist_mempool_test.go index 3c51752b9..1cc8d2a73 100644 --- a/mempool/clist_mempool_test.go +++ b/mempool/clist_mempool_test.go @@ -268,8 +268,8 @@ func NewSleepCounterApplication(f bool, i int) *SleepCounterApplication { return &SleepCounterApplication{counter.NewCounterApplication(f), wg} } -func (app *SleepCounterApplication) CheckTx(tx []byte) abci.ResponseCheckTx { - res := app.CounterApplication.CheckTx(tx) +func (app *SleepCounterApplication) CheckTx(req abci.RequestCheckTx) abci.ResponseCheckTx { + res := app.CounterApplication.CheckTx(req) app.wg.Wait() return res } @@ -301,20 +301,20 @@ func TestReapPriority(t *testing.T) { //for threshold := range seqReap { txs := mempool.ReapMaxBytesMaxGas(-1, -1) - if len(txs) >= threshold { + if len(txs) > threshold { str := fmt.Sprintf("Reap failed to have priority, %v > %v\n", len(txs), threshold) fmt.Print(str) testResult <- str } else { - fmt.Printf("Priority reaping: %v < %v\n", len(txs), threshold) + fmt.Printf("Priority reaping: %v <= %v\n", len(txs), threshold) } j += len(txs) - if err := mempool.Update(0, txs, nil, nil); err != nil { + if err := mempool.Update(0, txs, abciResponses(len(txs), abci.CodeTypeOK),nil, nil); err != nil { testResult <- err.Error() } for _, txBytes := range txs { - res, err := appConnCon.DeliverTxSync(txBytes) + res, err := appConnCon.DeliverTxSync(abci.RequestDeliverTx{Tx: txBytes}) if err != nil { testResult <- fmt.Sprintf("Client error committing tx: %v", err) } diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index 18e20a9ed..c7d31bcf1 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -98,9 +98,9 @@ func createConfig() *cfg.Config { c.RPC.CORSAllowedOrigins = []string{"https://tendermint.com/"} c.RPC.GRPCListenAddress = grpc c.TxIndex.IndexTags = "app.creator,tx.height" // see kvstore application - globalConfig.RPC.WebsocketPoolMaxSize = 1 - globalConfig.RPC.WebsocketPoolQueueSize = 1 - globalConfig.RPC.WebsocketPoolSpawnSize = 1 + c.RPC.WebsocketPoolMaxSize = 1 + c.RPC.WebsocketPoolQueueSize = 1 + c.RPC.WebsocketPoolSpawnSize = 1 return c } diff --git a/state/state_test.go b/state/state_test.go index c207c85aa..67886cb1e 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -92,8 +92,8 @@ func TestABCIResponsesSaveLoad1(t *testing.T) { // Build mock responses. block := makeBlock(state, 2) abciResponses := sm.NewABCIResponses(block) - abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Tags: nil} - abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Tags: nil} + abciResponses.DeliverTx[0] = &abci.ResponseDeliverTx{Data: []byte("foo"), Events: nil} + abciResponses.DeliverTx[1] = &abci.ResponseDeliverTx{Data: []byte("bar"), Log: "ok", Events: nil} abciResponses.EndBlock = &abci.ResponseEndBlock{ValidatorUpdates: []abci.ValidatorUpdate{ types.TM2PB.NewValidatorUpdate(ed25519.GenPrivKey().PubKey(), 10), @@ -979,7 +979,7 @@ func TestApplyUpdates(t *testing.T) { MaxAge: 66, }, }, - makeConsensusParams(1, 2, 3)}, + makeConsensusParams(1, 2, 66)}, } for i, tc := range cases { diff --git a/state/txindex/kv/kv_bench_test.go b/state/txindex/kv/kv_bench_test.go index eeb716f08..60685e949 100644 --- a/state/txindex/kv/kv_bench_test.go +++ b/state/txindex/kv/kv_bench_test.go @@ -9,7 +9,6 @@ import ( abci "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/types" ) @@ -60,7 +59,7 @@ func BenchmarkTxSearch(b *testing.B) { } } - txQuery := query.MustParse("transfer.address = 'address_43' AND transfer.amount = 50") + txQuery := "transfer.address = 'address_43' AND transfer.amount = 50" b.ResetTimer() diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index 0b32f132d..31ae29f52 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -129,7 +129,7 @@ func TestTxSearch(t *testing.T) { func TestTxSearchDeprecatedIndexing(t *testing.T) { allowedTags := []string{"account.number", "sender"} - indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags)) + indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags), EnableRangeQuery()) // index tx using events indexing (composite key) txResult1 := txResultWithEvents([]abci.Event{ diff --git a/types/vote_test.go b/types/vote_test.go index b6eb1f586..46373d61f 100644 --- a/types/vote_test.go +++ b/types/vote_test.go @@ -255,13 +255,13 @@ func TestMaxVoteBytes(t *testing.T) { func TestVoteString(t *testing.T) { str := examplePrecommit().String() - expected := `Vote{56789:6AF1F4111082 12345/02/2(Precommit) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` + expected := `Vote{56789:6AF1F4111082 12345/02/precommit(Precommit) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` if str != expected { t.Errorf("Got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str) } str2 := examplePrevote().String() - expected = `Vote{56789:6AF1F4111082 12345/02/1(Prevote) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` + expected = `Vote{56789:6AF1F4111082 12345/02/prevote(Prevote) 8B01023386C3 000000000000 @ 2017-12-25T03:00:01.234Z}` if str2 != expected { t.Errorf("Got unexpected string for Vote. Expected:\n%v\nGot:\n%v", expected, str2) } From 2383d10a0bdbed5d4984bb7bbd25ca1467aa1f2a Mon Sep 17 00:00:00 2001 From: fudongbai <296179868@qq.com> Date: Thu, 12 Sep 2019 10:34:38 +0800 Subject: [PATCH 187/211] fix unstable case --- blockchain/hot/candidate_test.go | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/blockchain/hot/candidate_test.go b/blockchain/hot/candidate_test.go index d76665479..4146d1899 100644 --- a/blockchain/hot/candidate_test.go +++ b/blockchain/hot/candidate_test.go @@ -234,18 +234,13 @@ func TestPickFromDecayedSet(t *testing.T) { sampleStream <- metricsEvent{Bad, p, 0} } candidates := candidatePool.pickFromDecayedSet(true) + total:=make(map[p2p.ID]bool) for _, c := range candidates { - for idx, tpid := range testPids { + for _, tpid := range testPids { if *c == tpid { - if len(testPids) > 1 { - testPids = append(testPids[:idx], testPids[idx+1:]...) - break - } else { - testPids = nil - break - } + total[tpid] = true } } } - assert.Nil(t, testPids) + assert.Equal(t, len(total),100) } From e25e26c0fb97fd6002b81bbb5b8890ab99473ec0 Mon Sep 17 00:00:00 2001 From: fudongbai <296179868@qq.com> Date: Thu, 12 Sep 2019 10:50:14 +0800 Subject: [PATCH 188/211] use attribute key as store key, try not introduce breaking change --- rpc/client/rpc_test.go | 6 ++-- rpc/test/helpers.go | 2 +- state/txindex/kv/kv.go | 5 +-- state/txindex/kv/kv_test.go | 66 ++++++++++++++++++------------------- 4 files changed, 40 insertions(+), 39 deletions(-) diff --git a/rpc/client/rpc_test.go b/rpc/client/rpc_test.go index de5e18f11..d87650df7 100644 --- a/rpc/client/rpc_test.go +++ b/rpc/client/rpc_test.go @@ -431,21 +431,21 @@ func TestTxSearch(t *testing.T) { require.Len(t, result.Txs, 0) // query using a tag (see kvstore application) - result, err = c.TxSearch("app.creator='Cosmoshi Netowoko'", false, 1, 30) + result, err = c.TxSearch("creator='Cosmoshi Netowoko'", false, 1, 30) require.Nil(t, err, "%+v", err) if len(result.Txs) == 0 { t.Fatal("expected a lot of transactions") } // query using a tag (see kvstore application) and height - result, err = c.TxSearch("app.creator='Cosmoshi Netowoko' AND tx.height<10000", true, 1, 30) + result, err = c.TxSearch("creator='Cosmoshi Netowoko' AND tx.height<10000", true, 1, 30) require.Nil(t, err, "%+v", err) if len(result.Txs) == 0 { t.Fatal("expected a lot of transactions") } // query a non existing tx with page 1 and txsPerPage 1 - result, err = c.TxSearch("app.creator='Cosmoshi Neetowoko'", true, 1, 1) + result, err = c.TxSearch("creator='Cosmoshi Neetowoko'", true, 1, 1) require.Nil(t, err, "%+v", err) require.Len(t, result.Txs, 0) } diff --git a/rpc/test/helpers.go b/rpc/test/helpers.go index c7d31bcf1..56617acf4 100644 --- a/rpc/test/helpers.go +++ b/rpc/test/helpers.go @@ -97,7 +97,7 @@ func createConfig() *cfg.Config { c.RPC.ListenAddress = rpc c.RPC.CORSAllowedOrigins = []string{"https://tendermint.com/"} c.RPC.GRPCListenAddress = grpc - c.TxIndex.IndexTags = "app.creator,tx.height" // see kvstore application + c.TxIndex.IndexTags = "creator,tx.height" // see kvstore application c.RPC.WebsocketPoolMaxSize = 1 c.RPC.WebsocketPoolQueueSize = 1 c.RPC.WebsocketPoolSpawnSize = 1 diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index a262ebb26..ec97d6012 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -156,8 +156,9 @@ func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.S if len(attr.Key) == 0 { continue } - - compositeTag := fmt.Sprintf("%s.%s", event.Type, string(attr.Key)) + // Notice, https://github.com/tendermint/tendermint/pull/3643 introduce a breaking change. + // We don't want to stop the world and reindex the data, so we choose attribute key as store key. + compositeTag := string(attr.Key) if txi.indexAllTags || cmn.StringInSlice(compositeTag, txi.tagsToIndex) { store.Set(keyForEvent(compositeTag, attr.Value, result), hash) } diff --git a/state/txindex/kv/kv_test.go b/state/txindex/kv/kv_test.go index 31ae29f52..3ecd374c0 100644 --- a/state/txindex/kv/kv_test.go +++ b/state/txindex/kv/kv_test.go @@ -62,7 +62,7 @@ func TestTxIndex(t *testing.T) { } func TestTxSearch(t *testing.T) { - allowedTags := []string{"account.number", "account.owner", "account.date"} + allowedTags := []string{"number", "owner", "date"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags), EnableRangeQuery()) txResult := txResultWithEvents([]abci.Event{ @@ -82,36 +82,36 @@ func TestTxSearch(t *testing.T) { // search by hash {fmt.Sprintf("tx.hash = '%X'", hash), 1}, // search by exact match (one tag) - {"account.number = 1", 1}, + {"number = 1", 1}, // search by exact match (two tags) - {"account.number = 1 AND account.owner = 'Ivan'", 1}, + {"number = 1 AND owner = 'Ivan'", 1}, // search by exact match (two tags) - {"account.number = 1 AND account.owner = 'Vlad'", 0}, - {"account.owner = 'Vlad' AND account.number = 1", 0}, - {"account.number >= 1 AND account.owner = 'Vlad'", 0}, - {"account.owner = 'Vlad' AND account.number >= 1", 0}, - {"account.number <= 0", 0}, - {"account.number <= 0 AND account.owner = 'Ivan'", 0}, + {"number = 1 AND owner = 'Vlad'", 0}, + {"owner = 'Vlad' AND number = 1", 0}, + {"number >= 1 AND owner = 'Vlad'", 0}, + {"owner = 'Vlad' AND number >= 1", 0}, + {"number <= 0", 0}, + {"number <= 0 AND owner = 'Ivan'", 0}, // search using a prefix of the stored value - {"account.owner = 'Iv'", 0}, + {"owner = 'Iv'", 0}, // search by range - {"account.number >= 1 AND account.number <= 5", 1}, + {"number >= 1 AND number <= 5", 1}, // search by range (lower bound) - {"account.number >= 1", 1}, + {"number >= 1", 1}, // search by range (upper bound) - {"account.number <= 5", 1}, + {"number <= 5", 1}, // search using not allowed tag {"not_allowed = 'boom'", 0}, // search for not existing tx result - {"account.number >= 2 AND account.number <= 5", 0}, + {"number >= 2 AND number <= 5", 0}, // search using not existing tag - {"account.date >= TIME 2013-05-03T14:45:00Z", 0}, + {"date >= TIME 2013-05-03T14:45:00Z", 0}, // search using CONTAINS - {"account.owner CONTAINS 'an'", 1}, + {"owner CONTAINS 'an'", 1}, // search for non existing value using CONTAINS - {"account.owner CONTAINS 'Vlad'", 0}, + {"owner CONTAINS 'Vlad'", 0}, // search using the wrong tag (of numeric type) using CONTAINS - {"account.number CONTAINS 'Iv'", 0}, + {"number CONTAINS 'Iv'", 0}, } for _, tc := range testCases { @@ -128,7 +128,7 @@ func TestTxSearch(t *testing.T) { } func TestTxSearchDeprecatedIndexing(t *testing.T) { - allowedTags := []string{"account.number", "sender"} + allowedTags := []string{"number", "sender"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags), EnableRangeQuery()) // index tx using events indexing (composite key) @@ -171,18 +171,18 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) { // search by hash {fmt.Sprintf("tx.hash = '%X'", hash2), []*types.TxResult{txResult2}}, // search by exact match (one tag) - {"account.number = 1", []*types.TxResult{txResult1}}, - {"account.number >= 1 AND account.number <= 5", []*types.TxResult{txResult1}}, + {"number = 1", []*types.TxResult{txResult1}}, + {"number >= 1 AND number <= 5", []*types.TxResult{txResult1}}, // search by range (lower bound) - {"account.number >= 1", []*types.TxResult{txResult1}}, + {"number >= 1", []*types.TxResult{txResult1}}, // search by range (upper bound) - {"account.number <= 5", []*types.TxResult{txResult1}}, + {"number <= 5", []*types.TxResult{txResult1}}, // search using not allowed tag {"not_allowed = 'boom'", []*types.TxResult{}}, // search for not existing tx result - {"account.number >= 2 AND account.number <= 5", []*types.TxResult{}}, + {"number >= 2 AND number <= 5", []*types.TxResult{}}, // search using not existing tag - {"account.date >= TIME 2013-05-03T14:45:00Z", []*types.TxResult{}}, + {"date >= TIME 2013-05-03T14:45:00Z", []*types.TxResult{}}, // search by deprecated tag {"sender = 'addr1'", []*types.TxResult{txResult2}}, } @@ -197,7 +197,7 @@ func TestTxSearchDeprecatedIndexing(t *testing.T) { } func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { - allowedTags := []string{"account.number"} + allowedTags := []string{"number"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags), EnableRangeQuery()) txResult := txResultWithEvents([]abci.Event{ @@ -208,7 +208,7 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { err := indexer.Index(txResult) require.NoError(t, err) - results, err := indexer.Search("account.number >= 1") + results, err := indexer.Search("number >= 1") assert.NoError(t, err) assert.Len(t, results, 1) @@ -216,7 +216,7 @@ func TestTxSearchOneTxWithMultipleSameTagsButDifferentValues(t *testing.T) { } func TestTxSearchMultipleTxs(t *testing.T) { - allowedTags := []string{"account.number", "account.number.id"} + allowedTags := []string{"number", "number.id"} indexer := NewTxIndex(db.NewMemDB(), IndexTags(allowedTags), EnableRangeQuery()) // indexed first, but bigger height (to test the order of transactions) @@ -262,7 +262,7 @@ func TestTxSearchMultipleTxs(t *testing.T) { err = indexer.Index(txResult4) require.NoError(t, err) - results, err := indexer.Search("account.number >= 1") + results, err := indexer.Search("number >= 1") assert.NoError(t, err) require.Len(t, results, 3) @@ -280,12 +280,12 @@ func TestIndexAllTags(t *testing.T) { err := indexer.Index(txResult) require.NoError(t, err) - results, err := indexer.Search("account.number >= 1") + results, err := indexer.Search("number >= 1") assert.NoError(t, err) assert.Len(t, results, 1) assert.Equal(t, []*types.TxResult{txResult}, results) - results, err = indexer.Search("account.owner = 'Ivan'") + results, err = indexer.Search("owner = 'Ivan'") assert.NoError(t, err) assert.Len(t, results, 1) assert.Equal(t, []*types.TxResult{txResult}, results) @@ -295,9 +295,9 @@ func TestIndexAllTags(t *testing.T) { func TestDisableRangeQuery(t *testing.T) { indexer := NewTxIndex(db.NewMemDB(), IndexAllTags()) - _, err := indexer.Search("account.number >= 1") + _, err := indexer.Search("number >= 1") assert.Error(t, err) - _, err = indexer.Search("account.number >= 1 AND account.sequence < 100 AND tx.height > 200 AND tx.height <= 300") + _, err = indexer.Search("number >= 1 AND sequence < 100 AND tx.height > 200 AND tx.height <= 300") assert.Error(t, err) } From 625122bfa8a0ddbf005b58cef080d4f1e36e2b08 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Mon, 16 Sep 2019 16:11:44 +0800 Subject: [PATCH 189/211] Add removed color bytes back --- libs/common/colors.go | 95 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 libs/common/colors.go diff --git a/libs/common/colors.go b/libs/common/colors.go new file mode 100644 index 000000000..89dda2c97 --- /dev/null +++ b/libs/common/colors.go @@ -0,0 +1,95 @@ +package common + +import ( + "fmt" + "strings" +) + +const ( + ANSIReset = "\x1b[0m" + ANSIBright = "\x1b[1m" + ANSIDim = "\x1b[2m" + ANSIUnderscore = "\x1b[4m" + ANSIBlink = "\x1b[5m" + ANSIReverse = "\x1b[7m" + ANSIHidden = "\x1b[8m" + + ANSIFgBlack = "\x1b[30m" + ANSIFgRed = "\x1b[31m" + ANSIFgGreen = "\x1b[32m" + ANSIFgYellow = "\x1b[33m" + ANSIFgBlue = "\x1b[34m" + ANSIFgMagenta = "\x1b[35m" + ANSIFgCyan = "\x1b[36m" + ANSIFgWhite = "\x1b[37m" + + ANSIBgBlack = "\x1b[40m" + ANSIBgRed = "\x1b[41m" + ANSIBgGreen = "\x1b[42m" + ANSIBgYellow = "\x1b[43m" + ANSIBgBlue = "\x1b[44m" + ANSIBgMagenta = "\x1b[45m" + ANSIBgCyan = "\x1b[46m" + ANSIBgWhite = "\x1b[47m" +) + +// color the string s with color 'color' +// unless s is already colored +func treat(s string, color string) string { + if len(s) > 2 && s[:2] == "\x1b[" { + return s + } + return color + s + ANSIReset +} + +func treatAll(color string, args ...interface{}) string { + parts := make([]string, 0, len(args)) + for _, arg := range args { + parts = append(parts, treat(fmt.Sprintf("%v", arg), color)) + } + return strings.Join(parts, "") +} + +func Black(args ...interface{}) string { + return treatAll(ANSIFgBlack, args...) +} + +func Red(args ...interface{}) string { + return treatAll(ANSIFgRed, args...) +} + +func Green(args ...interface{}) string { + return treatAll(ANSIFgGreen, args...) +} + +func Yellow(args ...interface{}) string { + return treatAll(ANSIFgYellow, args...) +} + +func Blue(args ...interface{}) string { + return treatAll(ANSIFgBlue, args...) +} + +func Magenta(args ...interface{}) string { + return treatAll(ANSIFgMagenta, args...) +} + +func Cyan(args ...interface{}) string { + return treatAll(ANSIFgCyan, args...) +} + +func White(args ...interface{}) string { + return treatAll(ANSIFgWhite, args...) +} + +func ColoredBytes(data []byte, textColor, bytesColor func(...interface{}) string) string { + s := "" + for _, b := range data { + if 0x21 <= b && b < 0x7F { + s += textColor(string(b)) + } else { + s += bytesColor(fmt.Sprintf("%02X", b)) + } + } + return s +} From 50b2cdff12dff2621523bb0a9748a2ec509bd26e Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Mon, 16 Sep 2019 20:32:32 +0800 Subject: [PATCH 190/211] Fix validator set issue for state sync --- state/execution.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/state/execution.go b/state/execution.go index 5f0fd2fa0..78dc0dbae 100644 --- a/state/execution.go +++ b/state/execution.go @@ -311,9 +311,15 @@ func getBeginBlockValidatorInfo(block *types.Block, stateDB dbm.DB) (abci.LastCo var lastValSet *types.ValidatorSet var err error if block.Height > 1 { - lastValSet, err = LoadValidators(stateDB, block.Height-1) - if err != nil { - panic(err) // shouldn't happen + state := LoadState(stateDB) + // for state sync, validator set can't be load from db and it should be equal to the validator set in state + if block.Height == state.LastBlockHeight + 1 { + lastValSet = state.Validators + } else { + lastValSet, err = LoadValidators(stateDB, block.Height-1) + if err != nil { + panic(err) // shouldn't happen + } } // Sanity check that commit length matches validator set size - From 79a8e4735cb2b6e58b89871667e3b144fb5a20b9 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Tue, 17 Sep 2019 11:13:00 +0800 Subject: [PATCH 191/211] Fix nil logger bug in SwitchToBlockchain v1 --- blockchain/v1/reactor.go | 1 + 1 file changed, 1 insertion(+) diff --git a/blockchain/v1/reactor.go b/blockchain/v1/reactor.go index da3eb2888..fbb0fdf6a 100644 --- a/blockchain/v1/reactor.go +++ b/blockchain/v1/reactor.go @@ -483,6 +483,7 @@ func (bcR *BlockchainReactor) SwitchToBlockchain(state *sm.State) { startHeight := state.LastBlockHeight + 1 fsm := NewFSM(startHeight, bcR) + fsm.SetLogger(bcR.Logger) bcR.fsm = fsm go bcR.poolRoutine() From cec9e42ef9caf344a36375214b7ad537f417d30f Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Tue, 17 Sep 2019 14:37:27 +0800 Subject: [PATCH 192/211] move stateSync creation before createBlockchainReactor creation --- node/node.go | 18 ++++++++++++------ snapshot/reactor.go | 4 ++-- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/node/node.go b/node/node.go index bef83b290..698fba903 100644 --- a/node/node.go +++ b/node/node.go @@ -501,6 +501,7 @@ func createSwitch(config *cfg.Config, peerFilters []p2p.PeerFilterFunc, mempoolReactor *mempl.Reactor, bcReactor p2p.Reactor, + stateReactor *snapshot.StateReactor, consensusReactor *consensus.ConsensusReactor, evidenceReactor *evidence.EvidenceReactor, nodeInfo p2p.NodeInfo, @@ -518,6 +519,9 @@ func createSwitch(config *cfg.Config, sw.AddReactor("BLOCKCHAIN", bcReactor) sw.AddReactor("CONSENSUS", consensusReactor) sw.AddReactor("EVIDENCE", evidenceReactor) + if stateReactor != nil { + sw.AddReactor("STATE", stateReactor) + } sw.SetNodeInfo(nodeInfo) sw.SetNodeKey(nodeKey) @@ -563,7 +567,7 @@ func createHotSyncReactorAndAddToSwitch(privValidator types.PrivValidator, block sw.AddReactor("HOT", hotSyncReactor) } -func createStateReactorAndAddToSwitch(txDB dbm.DB, proxyApp proxy.AppConns, blockStore *store.BlockStore, stateDB dbm.DB, config *cfg.Config, sw *p2p.Switch, logger log.Logger) { +func createStateReactor(txDB dbm.DB, proxyApp proxy.AppConns, blockStore *store.BlockStore, stateDB dbm.DB, config *cfg.Config, logger log.Logger) *snapshot.StateReactor { stateSyncLogger := logger.With("module", "statesync") snapshot.InitSnapshotManager(stateDB, txDB, blockStore, config.DBDir(), stateSyncLogger) @@ -571,7 +575,7 @@ func createStateReactorAndAddToSwitch(txDB dbm.DB, proxyApp proxy.AppConns, bloc // so the later reactor need read config.StateSyncHeight rather than a copied variable stateReactor := snapshot.NewStateReactor(stateDB, proxyApp.State(), config) stateReactor.SetLogger(stateSyncLogger) - sw.AddReactor("STATE", stateReactor) + return stateReactor } func createPEXReactorAndAddToSwitch(addrBook pex.AddrBook, config *cfg.Config, @@ -692,6 +696,11 @@ func NewNode(config *cfg.Config, sm.BlockExecutorWithMetrics(smMetrics), ) + var stateReactor *snapshot.StateReactor + if config.StateSyncReactor { + stateReactor = createStateReactor(txDB, proxyApp, blockStore, stateDB, config, logger) + } + // Make BlockchainReactor bcReactor, err := createBlockchainReactor(config, state, blockExec, blockStore, fastSync && (config.StateSyncHeight < 0 || !config.StateSyncReactor), logger) @@ -716,7 +725,7 @@ func NewNode(config *cfg.Config, // Setup Switch. p2pLogger := logger.With("module", "p2p") sw := createSwitch( - config, transport, p2pMetrics, peerFilters, mempoolReactor, bcReactor, + config, transport, p2pMetrics, peerFilters, mempoolReactor, bcReactor, stateReactor, consensusReactor, evidenceReactor, nodeInfo, nodeKey, p2pLogger, ) @@ -746,9 +755,6 @@ func NewNode(config *cfg.Config, if config.P2P.PexReactor { pexReactor = createPEXReactorAndAddToSwitch(addrBook, config, sw, logger) } - if config.StateSyncReactor { - createStateReactorAndAddToSwitch(txDB, proxyApp, blockStore, stateDB, config, sw, logger) - } if config.HotSyncReactor { createHotSyncReactorAndAddToSwitch(privValidator, blockExec, blockStore, eventBus, state, config, fastSync, htMetrics, sw, logger) diff --git a/snapshot/reactor.go b/snapshot/reactor.go index 4aca86d82..27dd4e765 100644 --- a/snapshot/reactor.go +++ b/snapshot/reactor.go @@ -3,7 +3,6 @@ package snapshot import ( "crypto/sha256" "fmt" - "github.com/tendermint/tendermint/state" "os" "path/filepath" "reflect" @@ -18,6 +17,7 @@ import ( "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" "github.com/tendermint/tendermint/proxy" + sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) @@ -51,7 +51,7 @@ const ( type fastSyncReactor interface { // for when we switch from blockchain reactor and fast sync to // the consensus machine - SwitchToBlockchain(*state.State) + SwitchToBlockchain(*sm.State) } type peerError struct { From 9a5e7ec7f6c6f04f4c3b521d4f784d88933b8d38 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Wed, 18 Sep 2019 14:20:04 +0800 Subject: [PATCH 193/211] add deprecated abci types --- abci/types/result.go | 10 + abci/types/types.pb.go | 1765 ++++++++++++++++++++++++++++------- abci/types/types.proto | 22 + abci/types/typespb_test.go | 248 +++++ abci/types/util.go | 40 + config/config.go | 2 +- rpc/core/types/responses.go | 16 + state/store.go | 23 +- state/txindex/kv/kv.go | 18 +- types/tx.go | 8 + 10 files changed, 1829 insertions(+), 323 deletions(-) diff --git a/abci/types/result.go b/abci/types/result.go index dbf409f4c..d0deb42d7 100644 --- a/abci/types/result.go +++ b/abci/types/result.go @@ -31,6 +31,16 @@ func (r ResponseDeliverTx) IsErr() bool { return r.Code != CodeTypeOK } +// IsOK returns true if Code is OK. +func (r ResponseDeliverTxDeprecated) IsOK() bool { + return r.Code == CodeTypeOK +} + +// IsErr returns true if Code is something other than OK. +func (r ResponseDeliverTxDeprecated) IsErr() bool { + return r.Code != CodeTypeOK +} + // IsOK returns true if Code is OK. func (r ResponseQuery) IsOK() bool { return r.Code == CodeTypeOK diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index 1e7973ba4..ad525468d 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -58,7 +58,7 @@ func (x CheckTxType) String() string { return proto.EnumName(CheckTxType_name, int32(x)) } func (CheckTxType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{0} + return fileDescriptor_types_7fd6485bbc04efd7, []int{0} } type Request struct { @@ -84,7 +84,7 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{0} + return fileDescriptor_types_7fd6485bbc04efd7, []int{0} } func (m *Request) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -506,7 +506,7 @@ func (m *RequestEcho) Reset() { *m = RequestEcho{} } func (m *RequestEcho) String() string { return proto.CompactTextString(m) } func (*RequestEcho) ProtoMessage() {} func (*RequestEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{1} + return fileDescriptor_types_7fd6485bbc04efd7, []int{1} } func (m *RequestEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -552,7 +552,7 @@ func (m *RequestFlush) Reset() { *m = RequestFlush{} } func (m *RequestFlush) String() string { return proto.CompactTextString(m) } func (*RequestFlush) ProtoMessage() {} func (*RequestFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{2} + return fileDescriptor_types_7fd6485bbc04efd7, []int{2} } func (m *RequestFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -594,7 +594,7 @@ func (m *RequestInfo) Reset() { *m = RequestInfo{} } func (m *RequestInfo) String() string { return proto.CompactTextString(m) } func (*RequestInfo) ProtoMessage() {} func (*RequestInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{3} + return fileDescriptor_types_7fd6485bbc04efd7, []int{3} } func (m *RequestInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -657,7 +657,7 @@ func (m *RequestSetOption) Reset() { *m = RequestSetOption{} } func (m *RequestSetOption) String() string { return proto.CompactTextString(m) } func (*RequestSetOption) ProtoMessage() {} func (*RequestSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{4} + return fileDescriptor_types_7fd6485bbc04efd7, []int{4} } func (m *RequestSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -715,7 +715,7 @@ func (m *RequestInitChain) Reset() { *m = RequestInitChain{} } func (m *RequestInitChain) String() string { return proto.CompactTextString(m) } func (*RequestInitChain) ProtoMessage() {} func (*RequestInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{5} + return fileDescriptor_types_7fd6485bbc04efd7, []int{5} } func (m *RequestInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -793,7 +793,7 @@ func (m *RequestQuery) Reset() { *m = RequestQuery{} } func (m *RequestQuery) String() string { return proto.CompactTextString(m) } func (*RequestQuery) ProtoMessage() {} func (*RequestQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{6} + return fileDescriptor_types_7fd6485bbc04efd7, []int{6} } func (m *RequestQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -864,7 +864,7 @@ func (m *RequestBeginBlock) Reset() { *m = RequestBeginBlock{} } func (m *RequestBeginBlock) String() string { return proto.CompactTextString(m) } func (*RequestBeginBlock) ProtoMessage() {} func (*RequestBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{7} + return fileDescriptor_types_7fd6485bbc04efd7, []int{7} } func (m *RequestBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -933,7 +933,7 @@ func (m *RequestCheckTx) Reset() { *m = RequestCheckTx{} } func (m *RequestCheckTx) String() string { return proto.CompactTextString(m) } func (*RequestCheckTx) ProtoMessage() {} func (*RequestCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{8} + return fileDescriptor_types_7fd6485bbc04efd7, []int{8} } func (m *RequestCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -987,7 +987,7 @@ func (m *RequestDeliverTx) Reset() { *m = RequestDeliverTx{} } func (m *RequestDeliverTx) String() string { return proto.CompactTextString(m) } func (*RequestDeliverTx) ProtoMessage() {} func (*RequestDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{9} + return fileDescriptor_types_7fd6485bbc04efd7, []int{9} } func (m *RequestDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1034,7 +1034,7 @@ func (m *RequestEndBlock) Reset() { *m = RequestEndBlock{} } func (m *RequestEndBlock) String() string { return proto.CompactTextString(m) } func (*RequestEndBlock) ProtoMessage() {} func (*RequestEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{10} + return fileDescriptor_types_7fd6485bbc04efd7, []int{10} } func (m *RequestEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1080,7 +1080,7 @@ func (m *RequestCommit) Reset() { *m = RequestCommit{} } func (m *RequestCommit) String() string { return proto.CompactTextString(m) } func (*RequestCommit) ProtoMessage() {} func (*RequestCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{11} + return fileDescriptor_types_7fd6485bbc04efd7, []int{11} } func (m *RequestCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1133,7 +1133,7 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{12} + return fileDescriptor_types_7fd6485bbc04efd7, []int{12} } func (m *Response) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1586,7 +1586,7 @@ func (m *ResponseException) Reset() { *m = ResponseException{} } func (m *ResponseException) String() string { return proto.CompactTextString(m) } func (*ResponseException) ProtoMessage() {} func (*ResponseException) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{13} + return fileDescriptor_types_7fd6485bbc04efd7, []int{13} } func (m *ResponseException) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1633,7 +1633,7 @@ func (m *ResponseEcho) Reset() { *m = ResponseEcho{} } func (m *ResponseEcho) String() string { return proto.CompactTextString(m) } func (*ResponseEcho) ProtoMessage() {} func (*ResponseEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{14} + return fileDescriptor_types_7fd6485bbc04efd7, []int{14} } func (m *ResponseEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1679,7 +1679,7 @@ func (m *ResponseFlush) Reset() { *m = ResponseFlush{} } func (m *ResponseFlush) String() string { return proto.CompactTextString(m) } func (*ResponseFlush) ProtoMessage() {} func (*ResponseFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{15} + return fileDescriptor_types_7fd6485bbc04efd7, []int{15} } func (m *ResponseFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1723,7 +1723,7 @@ func (m *ResponseInfo) Reset() { *m = ResponseInfo{} } func (m *ResponseInfo) String() string { return proto.CompactTextString(m) } func (*ResponseInfo) ProtoMessage() {} func (*ResponseInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{16} + return fileDescriptor_types_7fd6485bbc04efd7, []int{16} } func (m *ResponseInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1802,7 +1802,7 @@ func (m *ResponseSetOption) Reset() { *m = ResponseSetOption{} } func (m *ResponseSetOption) String() string { return proto.CompactTextString(m) } func (*ResponseSetOption) ProtoMessage() {} func (*ResponseSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{17} + return fileDescriptor_types_7fd6485bbc04efd7, []int{17} } func (m *ResponseSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1864,7 +1864,7 @@ func (m *ResponseInitChain) Reset() { *m = ResponseInitChain{} } func (m *ResponseInitChain) String() string { return proto.CompactTextString(m) } func (*ResponseInitChain) ProtoMessage() {} func (*ResponseInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{18} + return fileDescriptor_types_7fd6485bbc04efd7, []int{18} } func (m *ResponseInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1927,7 +1927,7 @@ func (m *ResponseQuery) Reset() { *m = ResponseQuery{} } func (m *ResponseQuery) String() string { return proto.CompactTextString(m) } func (*ResponseQuery) ProtoMessage() {} func (*ResponseQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{19} + return fileDescriptor_types_7fd6485bbc04efd7, []int{19} } func (m *ResponseQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2030,7 +2030,7 @@ func (m *ResponseBeginBlock) Reset() { *m = ResponseBeginBlock{} } func (m *ResponseBeginBlock) String() string { return proto.CompactTextString(m) } func (*ResponseBeginBlock) ProtoMessage() {} func (*ResponseBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{20} + return fileDescriptor_types_7fd6485bbc04efd7, []int{20} } func (m *ResponseBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2084,7 +2084,7 @@ func (m *ResponseCheckTx) Reset() { *m = ResponseCheckTx{} } func (m *ResponseCheckTx) String() string { return proto.CompactTextString(m) } func (*ResponseCheckTx) ProtoMessage() {} func (*ResponseCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{21} + return fileDescriptor_types_7fd6485bbc04efd7, []int{21} } func (m *ResponseCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2169,6 +2169,109 @@ func (m *ResponseCheckTx) GetCodespace() string { return "" } +type ResponseCheckTxDeprecated struct { + Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Log string `protobuf:"bytes,3,opt,name=log,proto3" json:"log,omitempty"` + Info string `protobuf:"bytes,4,opt,name=info,proto3" json:"info,omitempty"` + GasWanted int64 `protobuf:"varint,5,opt,name=gas_wanted,json=gasWanted,proto3" json:"gas_wanted,omitempty"` + GasUsed int64 `protobuf:"varint,6,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` + Tags []common.KVPair `protobuf:"bytes,7,rep,name=tags" json:"tags,omitempty"` + Codespace string `protobuf:"bytes,8,opt,name=codespace,proto3" json:"codespace,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ResponseCheckTxDeprecated) Reset() { *m = ResponseCheckTxDeprecated{} } +func (m *ResponseCheckTxDeprecated) String() string { return proto.CompactTextString(m) } +func (*ResponseCheckTxDeprecated) ProtoMessage() {} +func (*ResponseCheckTxDeprecated) Descriptor() ([]byte, []int) { + return fileDescriptor_types_7fd6485bbc04efd7, []int{22} +} +func (m *ResponseCheckTxDeprecated) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ResponseCheckTxDeprecated) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ResponseCheckTxDeprecated.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (dst *ResponseCheckTxDeprecated) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResponseCheckTxDeprecated.Merge(dst, src) +} +func (m *ResponseCheckTxDeprecated) XXX_Size() int { + return m.Size() +} +func (m *ResponseCheckTxDeprecated) XXX_DiscardUnknown() { + xxx_messageInfo_ResponseCheckTxDeprecated.DiscardUnknown(m) +} + +var xxx_messageInfo_ResponseCheckTxDeprecated proto.InternalMessageInfo + +func (m *ResponseCheckTxDeprecated) GetCode() uint32 { + if m != nil { + return m.Code + } + return 0 +} + +func (m *ResponseCheckTxDeprecated) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +func (m *ResponseCheckTxDeprecated) GetLog() string { + if m != nil { + return m.Log + } + return "" +} + +func (m *ResponseCheckTxDeprecated) GetInfo() string { + if m != nil { + return m.Info + } + return "" +} + +func (m *ResponseCheckTxDeprecated) GetGasWanted() int64 { + if m != nil { + return m.GasWanted + } + return 0 +} + +func (m *ResponseCheckTxDeprecated) GetGasUsed() int64 { + if m != nil { + return m.GasUsed + } + return 0 +} + +func (m *ResponseCheckTxDeprecated) GetTags() []common.KVPair { + if m != nil { + return m.Tags + } + return nil +} + +func (m *ResponseCheckTxDeprecated) GetCodespace() string { + if m != nil { + return m.Codespace + } + return "" +} + type ResponseDeliverTx struct { Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` @@ -2187,7 +2290,7 @@ func (m *ResponseDeliverTx) Reset() { *m = ResponseDeliverTx{} } func (m *ResponseDeliverTx) String() string { return proto.CompactTextString(m) } func (*ResponseDeliverTx) ProtoMessage() {} func (*ResponseDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{22} + return fileDescriptor_types_7fd6485bbc04efd7, []int{23} } func (m *ResponseDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2272,6 +2375,109 @@ func (m *ResponseDeliverTx) GetCodespace() string { return "" } +type ResponseDeliverTxDeprecated struct { + Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` + Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` + Log string `protobuf:"bytes,3,opt,name=log,proto3" json:"log,omitempty"` + Info string `protobuf:"bytes,4,opt,name=info,proto3" json:"info,omitempty"` + GasWanted int64 `protobuf:"varint,5,opt,name=gas_wanted,json=gasWanted,proto3" json:"gas_wanted,omitempty"` + GasUsed int64 `protobuf:"varint,6,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"` + Tags []common.KVPair `protobuf:"bytes,7,rep,name=tags" json:"tags,omitempty"` + Codespace string `protobuf:"bytes,8,opt,name=codespace,proto3" json:"codespace,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ResponseDeliverTxDeprecated) Reset() { *m = ResponseDeliverTxDeprecated{} } +func (m *ResponseDeliverTxDeprecated) String() string { return proto.CompactTextString(m) } +func (*ResponseDeliverTxDeprecated) ProtoMessage() {} +func (*ResponseDeliverTxDeprecated) Descriptor() ([]byte, []int) { + return fileDescriptor_types_7fd6485bbc04efd7, []int{24} +} +func (m *ResponseDeliverTxDeprecated) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ResponseDeliverTxDeprecated) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ResponseDeliverTxDeprecated.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (dst *ResponseDeliverTxDeprecated) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResponseDeliverTxDeprecated.Merge(dst, src) +} +func (m *ResponseDeliverTxDeprecated) XXX_Size() int { + return m.Size() +} +func (m *ResponseDeliverTxDeprecated) XXX_DiscardUnknown() { + xxx_messageInfo_ResponseDeliverTxDeprecated.DiscardUnknown(m) +} + +var xxx_messageInfo_ResponseDeliverTxDeprecated proto.InternalMessageInfo + +func (m *ResponseDeliverTxDeprecated) GetCode() uint32 { + if m != nil { + return m.Code + } + return 0 +} + +func (m *ResponseDeliverTxDeprecated) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +func (m *ResponseDeliverTxDeprecated) GetLog() string { + if m != nil { + return m.Log + } + return "" +} + +func (m *ResponseDeliverTxDeprecated) GetInfo() string { + if m != nil { + return m.Info + } + return "" +} + +func (m *ResponseDeliverTxDeprecated) GetGasWanted() int64 { + if m != nil { + return m.GasWanted + } + return 0 +} + +func (m *ResponseDeliverTxDeprecated) GetGasUsed() int64 { + if m != nil { + return m.GasUsed + } + return 0 +} + +func (m *ResponseDeliverTxDeprecated) GetTags() []common.KVPair { + if m != nil { + return m.Tags + } + return nil +} + +func (m *ResponseDeliverTxDeprecated) GetCodespace() string { + if m != nil { + return m.Codespace + } + return "" +} + type ResponseEndBlock struct { ValidatorUpdates []ValidatorUpdate `protobuf:"bytes,1,rep,name=validator_updates,json=validatorUpdates" json:"validator_updates"` ConsensusParamUpdates *ConsensusParams `protobuf:"bytes,2,opt,name=consensus_param_updates,json=consensusParamUpdates" json:"consensus_param_updates,omitempty"` @@ -2285,7 +2491,7 @@ func (m *ResponseEndBlock) Reset() { *m = ResponseEndBlock{} } func (m *ResponseEndBlock) String() string { return proto.CompactTextString(m) } func (*ResponseEndBlock) ProtoMessage() {} func (*ResponseEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{23} + return fileDescriptor_types_7fd6485bbc04efd7, []int{25} } func (m *ResponseEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2347,7 +2553,7 @@ func (m *ResponseCommit) Reset() { *m = ResponseCommit{} } func (m *ResponseCommit) String() string { return proto.CompactTextString(m) } func (*ResponseCommit) ProtoMessage() {} func (*ResponseCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{24} + return fileDescriptor_types_7fd6485bbc04efd7, []int{26} } func (m *ResponseCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2398,7 +2604,7 @@ func (m *ConsensusParams) Reset() { *m = ConsensusParams{} } func (m *ConsensusParams) String() string { return proto.CompactTextString(m) } func (*ConsensusParams) ProtoMessage() {} func (*ConsensusParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{25} + return fileDescriptor_types_7fd6485bbc04efd7, []int{27} } func (m *ConsensusParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2462,7 +2668,7 @@ func (m *BlockSizeParams) Reset() { *m = BlockSizeParams{} } func (m *BlockSizeParams) String() string { return proto.CompactTextString(m) } func (*BlockSizeParams) ProtoMessage() {} func (*BlockSizeParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{26} + return fileDescriptor_types_7fd6485bbc04efd7, []int{28} } func (m *BlockSizeParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2518,7 +2724,7 @@ func (m *EvidenceParams) Reset() { *m = EvidenceParams{} } func (m *EvidenceParams) String() string { return proto.CompactTextString(m) } func (*EvidenceParams) ProtoMessage() {} func (*EvidenceParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{27} + return fileDescriptor_types_7fd6485bbc04efd7, []int{29} } func (m *EvidenceParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2566,7 +2772,7 @@ func (m *ValidatorParams) Reset() { *m = ValidatorParams{} } func (m *ValidatorParams) String() string { return proto.CompactTextString(m) } func (*ValidatorParams) ProtoMessage() {} func (*ValidatorParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{28} + return fileDescriptor_types_7fd6485bbc04efd7, []int{30} } func (m *ValidatorParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2614,7 +2820,7 @@ func (m *LastCommitInfo) Reset() { *m = LastCommitInfo{} } func (m *LastCommitInfo) String() string { return proto.CompactTextString(m) } func (*LastCommitInfo) ProtoMessage() {} func (*LastCommitInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{29} + return fileDescriptor_types_7fd6485bbc04efd7, []int{31} } func (m *LastCommitInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2669,7 +2875,7 @@ func (m *Event) Reset() { *m = Event{} } func (m *Event) String() string { return proto.CompactTextString(m) } func (*Event) ProtoMessage() {} func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{30} + return fileDescriptor_types_7fd6485bbc04efd7, []int{32} } func (m *Event) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2743,7 +2949,7 @@ func (m *Header) Reset() { *m = Header{} } func (m *Header) String() string { return proto.CompactTextString(m) } func (*Header) ProtoMessage() {} func (*Header) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{31} + return fileDescriptor_types_7fd6485bbc04efd7, []int{33} } func (m *Header) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2896,7 +3102,7 @@ func (m *Version) Reset() { *m = Version{} } func (m *Version) String() string { return proto.CompactTextString(m) } func (*Version) ProtoMessage() {} func (*Version) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{32} + return fileDescriptor_types_7fd6485bbc04efd7, []int{34} } func (m *Version) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2951,7 +3157,7 @@ func (m *BlockID) Reset() { *m = BlockID{} } func (m *BlockID) String() string { return proto.CompactTextString(m) } func (*BlockID) ProtoMessage() {} func (*BlockID) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{33} + return fileDescriptor_types_7fd6485bbc04efd7, []int{35} } func (m *BlockID) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3006,7 +3212,7 @@ func (m *PartSetHeader) Reset() { *m = PartSetHeader{} } func (m *PartSetHeader) String() string { return proto.CompactTextString(m) } func (*PartSetHeader) ProtoMessage() {} func (*PartSetHeader) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{34} + return fileDescriptor_types_7fd6485bbc04efd7, []int{36} } func (m *PartSetHeader) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3063,7 +3269,7 @@ func (m *Validator) Reset() { *m = Validator{} } func (m *Validator) String() string { return proto.CompactTextString(m) } func (*Validator) ProtoMessage() {} func (*Validator) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{35} + return fileDescriptor_types_7fd6485bbc04efd7, []int{37} } func (m *Validator) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3119,7 +3325,7 @@ func (m *ValidatorUpdate) Reset() { *m = ValidatorUpdate{} } func (m *ValidatorUpdate) String() string { return proto.CompactTextString(m) } func (*ValidatorUpdate) ProtoMessage() {} func (*ValidatorUpdate) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{36} + return fileDescriptor_types_7fd6485bbc04efd7, []int{38} } func (m *ValidatorUpdate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3175,7 +3381,7 @@ func (m *VoteInfo) Reset() { *m = VoteInfo{} } func (m *VoteInfo) String() string { return proto.CompactTextString(m) } func (*VoteInfo) ProtoMessage() {} func (*VoteInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{37} + return fileDescriptor_types_7fd6485bbc04efd7, []int{39} } func (m *VoteInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3230,7 +3436,7 @@ func (m *PubKey) Reset() { *m = PubKey{} } func (m *PubKey) String() string { return proto.CompactTextString(m) } func (*PubKey) ProtoMessage() {} func (*PubKey) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{38} + return fileDescriptor_types_7fd6485bbc04efd7, []int{40} } func (m *PubKey) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3288,7 +3494,7 @@ func (m *Evidence) Reset() { *m = Evidence{} } func (m *Evidence) String() string { return proto.CompactTextString(m) } func (*Evidence) ProtoMessage() {} func (*Evidence) Descriptor() ([]byte, []int) { - return fileDescriptor_types_b4cfdaa127d201bd, []int{39} + return fileDescriptor_types_7fd6485bbc04efd7, []int{41} } func (m *Evidence) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3397,8 +3603,12 @@ func init() { golang_proto.RegisterType((*ResponseBeginBlock)(nil), "types.ResponseBeginBlock") proto.RegisterType((*ResponseCheckTx)(nil), "types.ResponseCheckTx") golang_proto.RegisterType((*ResponseCheckTx)(nil), "types.ResponseCheckTx") + proto.RegisterType((*ResponseCheckTxDeprecated)(nil), "types.ResponseCheckTxDeprecated") + golang_proto.RegisterType((*ResponseCheckTxDeprecated)(nil), "types.ResponseCheckTxDeprecated") proto.RegisterType((*ResponseDeliverTx)(nil), "types.ResponseDeliverTx") golang_proto.RegisterType((*ResponseDeliverTx)(nil), "types.ResponseDeliverTx") + proto.RegisterType((*ResponseDeliverTxDeprecated)(nil), "types.ResponseDeliverTxDeprecated") + golang_proto.RegisterType((*ResponseDeliverTxDeprecated)(nil), "types.ResponseDeliverTxDeprecated") proto.RegisterType((*ResponseEndBlock)(nil), "types.ResponseEndBlock") golang_proto.RegisterType((*ResponseEndBlock)(nil), "types.ResponseEndBlock") proto.RegisterType((*ResponseCommit)(nil), "types.ResponseCommit") @@ -4718,6 +4928,59 @@ func (this *ResponseCheckTx) Equal(that interface{}) bool { } return true } +func (this *ResponseCheckTxDeprecated) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*ResponseCheckTxDeprecated) + if !ok { + that2, ok := that.(ResponseCheckTxDeprecated) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Code != that1.Code { + return false + } + if !bytes.Equal(this.Data, that1.Data) { + return false + } + if this.Log != that1.Log { + return false + } + if this.Info != that1.Info { + return false + } + if this.GasWanted != that1.GasWanted { + return false + } + if this.GasUsed != that1.GasUsed { + return false + } + if len(this.Tags) != len(that1.Tags) { + return false + } + for i := range this.Tags { + if !this.Tags[i].Equal(&that1.Tags[i]) { + return false + } + } + if this.Codespace != that1.Codespace { + return false + } + if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { + return false + } + return true +} func (this *ResponseDeliverTx) Equal(that interface{}) bool { if that == nil { return this == nil @@ -4771,6 +5034,59 @@ func (this *ResponseDeliverTx) Equal(that interface{}) bool { } return true } +func (this *ResponseDeliverTxDeprecated) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*ResponseDeliverTxDeprecated) + if !ok { + that2, ok := that.(ResponseDeliverTxDeprecated) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if this.Code != that1.Code { + return false + } + if !bytes.Equal(this.Data, that1.Data) { + return false + } + if this.Log != that1.Log { + return false + } + if this.Info != that1.Info { + return false + } + if this.GasWanted != that1.GasWanted { + return false + } + if this.GasUsed != that1.GasUsed { + return false + } + if len(this.Tags) != len(that1.Tags) { + return false + } + for i := range this.Tags { + if !this.Tags[i].Equal(&that1.Tags[i]) { + return false + } + } + if this.Codespace != that1.Codespace { + return false + } + if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { + return false + } + return true +} func (this *ResponseEndBlock) Equal(that interface{}) bool { if that == nil { return this == nil @@ -6913,7 +7229,7 @@ func (m *ResponseCheckTx) MarshalTo(dAtA []byte) (int, error) { return i, nil } -func (m *ResponseDeliverTx) Marshal() (dAtA []byte, err error) { +func (m *ResponseCheckTxDeprecated) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) @@ -6923,7 +7239,7 @@ func (m *ResponseDeliverTx) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *ResponseDeliverTx) MarshalTo(dAtA []byte) (int, error) { +func (m *ResponseCheckTxDeprecated) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int @@ -6961,8 +7277,8 @@ func (m *ResponseDeliverTx) MarshalTo(dAtA []byte) (int, error) { i++ i = encodeVarintTypes(dAtA, i, uint64(m.GasUsed)) } - if len(m.Events) > 0 { - for _, msg := range m.Events { + if len(m.Tags) > 0 { + for _, msg := range m.Tags { dAtA[i] = 0x3a i++ i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) @@ -6985,7 +7301,7 @@ func (m *ResponseDeliverTx) MarshalTo(dAtA []byte) (int, error) { return i, nil } -func (m *ResponseEndBlock) Marshal() (dAtA []byte, err error) { +func (m *ResponseDeliverTx) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) n, err := m.MarshalTo(dAtA) @@ -6995,23 +7311,167 @@ func (m *ResponseEndBlock) Marshal() (dAtA []byte, err error) { return dAtA[:n], nil } -func (m *ResponseEndBlock) MarshalTo(dAtA []byte) (int, error) { +func (m *ResponseDeliverTx) MarshalTo(dAtA []byte) (int, error) { var i int _ = i var l int _ = l - if len(m.ValidatorUpdates) > 0 { - for _, msg := range m.ValidatorUpdates { - dAtA[i] = 0xa - i++ - i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) - n, err := msg.MarshalTo(dAtA[i:]) - if err != nil { - return 0, err - } - i += n - } - } + if m.Code != 0 { + dAtA[i] = 0x8 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.Code)) + } + if len(m.Data) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Data))) + i += copy(dAtA[i:], m.Data) + } + if len(m.Log) > 0 { + dAtA[i] = 0x1a + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Log))) + i += copy(dAtA[i:], m.Log) + } + if len(m.Info) > 0 { + dAtA[i] = 0x22 + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Info))) + i += copy(dAtA[i:], m.Info) + } + if m.GasWanted != 0 { + dAtA[i] = 0x28 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.GasWanted)) + } + if m.GasUsed != 0 { + dAtA[i] = 0x30 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.GasUsed)) + } + if len(m.Events) > 0 { + for _, msg := range m.Events { + dAtA[i] = 0x3a + i++ + i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if len(m.Codespace) > 0 { + dAtA[i] = 0x42 + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Codespace))) + i += copy(dAtA[i:], m.Codespace) + } + if m.XXX_unrecognized != nil { + i += copy(dAtA[i:], m.XXX_unrecognized) + } + return i, nil +} + +func (m *ResponseDeliverTxDeprecated) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResponseDeliverTxDeprecated) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if m.Code != 0 { + dAtA[i] = 0x8 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.Code)) + } + if len(m.Data) > 0 { + dAtA[i] = 0x12 + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Data))) + i += copy(dAtA[i:], m.Data) + } + if len(m.Log) > 0 { + dAtA[i] = 0x1a + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Log))) + i += copy(dAtA[i:], m.Log) + } + if len(m.Info) > 0 { + dAtA[i] = 0x22 + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Info))) + i += copy(dAtA[i:], m.Info) + } + if m.GasWanted != 0 { + dAtA[i] = 0x28 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.GasWanted)) + } + if m.GasUsed != 0 { + dAtA[i] = 0x30 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.GasUsed)) + } + if len(m.Tags) > 0 { + for _, msg := range m.Tags { + dAtA[i] = 0x3a + i++ + i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if len(m.Codespace) > 0 { + dAtA[i] = 0x42 + i++ + i = encodeVarintTypes(dAtA, i, uint64(len(m.Codespace))) + i += copy(dAtA[i:], m.Codespace) + } + if m.XXX_unrecognized != nil { + i += copy(dAtA[i:], m.XXX_unrecognized) + } + return i, nil +} + +func (m *ResponseEndBlock) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResponseEndBlock) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.ValidatorUpdates) > 0 { + for _, msg := range m.ValidatorUpdates { + dAtA[i] = 0xa + i++ + i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } if m.ConsensusParamUpdates != nil { dAtA[i] = 0x12 i++ @@ -8205,8 +8665,8 @@ func NewPopulatedResponseCheckTx(r randyTypes, easy bool) *ResponseCheckTx { return this } -func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { - this := &ResponseDeliverTx{} +func NewPopulatedResponseCheckTxDeprecated(r randyTypes, easy bool) *ResponseCheckTxDeprecated { + this := &ResponseCheckTxDeprecated{} this.Code = uint32(r.Uint32()) v23 := r.Intn(100) this.Data = make([]byte, v23) @@ -8225,10 +8685,76 @@ func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { } if r.Intn(10) != 0 { v24 := r.Intn(5) - this.Events = make([]Event, v24) + this.Tags = make([]common.KVPair, v24) for i := 0; i < v24; i++ { - v25 := NewPopulatedEvent(r, easy) - this.Events[i] = *v25 + v25 := common.NewPopulatedKVPair(r, easy) + this.Tags[i] = *v25 + } + } + this.Codespace = string(randStringTypes(r)) + if !easy && r.Intn(10) != 0 { + this.XXX_unrecognized = randUnrecognizedTypes(r, 9) + } + return this +} + +func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { + this := &ResponseDeliverTx{} + this.Code = uint32(r.Uint32()) + v26 := r.Intn(100) + this.Data = make([]byte, v26) + for i := 0; i < v26; i++ { + this.Data[i] = byte(r.Intn(256)) + } + this.Log = string(randStringTypes(r)) + this.Info = string(randStringTypes(r)) + this.GasWanted = int64(r.Int63()) + if r.Intn(2) == 0 { + this.GasWanted *= -1 + } + this.GasUsed = int64(r.Int63()) + if r.Intn(2) == 0 { + this.GasUsed *= -1 + } + if r.Intn(10) != 0 { + v27 := r.Intn(5) + this.Events = make([]Event, v27) + for i := 0; i < v27; i++ { + v28 := NewPopulatedEvent(r, easy) + this.Events[i] = *v28 + } + } + this.Codespace = string(randStringTypes(r)) + if !easy && r.Intn(10) != 0 { + this.XXX_unrecognized = randUnrecognizedTypes(r, 9) + } + return this +} + +func NewPopulatedResponseDeliverTxDeprecated(r randyTypes, easy bool) *ResponseDeliverTxDeprecated { + this := &ResponseDeliverTxDeprecated{} + this.Code = uint32(r.Uint32()) + v29 := r.Intn(100) + this.Data = make([]byte, v29) + for i := 0; i < v29; i++ { + this.Data[i] = byte(r.Intn(256)) + } + this.Log = string(randStringTypes(r)) + this.Info = string(randStringTypes(r)) + this.GasWanted = int64(r.Int63()) + if r.Intn(2) == 0 { + this.GasWanted *= -1 + } + this.GasUsed = int64(r.Int63()) + if r.Intn(2) == 0 { + this.GasUsed *= -1 + } + if r.Intn(10) != 0 { + v30 := r.Intn(5) + this.Tags = make([]common.KVPair, v30) + for i := 0; i < v30; i++ { + v31 := common.NewPopulatedKVPair(r, easy) + this.Tags[i] = *v31 } } this.Codespace = string(randStringTypes(r)) @@ -8241,22 +8767,22 @@ func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { func NewPopulatedResponseEndBlock(r randyTypes, easy bool) *ResponseEndBlock { this := &ResponseEndBlock{} if r.Intn(10) != 0 { - v26 := r.Intn(5) - this.ValidatorUpdates = make([]ValidatorUpdate, v26) - for i := 0; i < v26; i++ { - v27 := NewPopulatedValidatorUpdate(r, easy) - this.ValidatorUpdates[i] = *v27 + v32 := r.Intn(5) + this.ValidatorUpdates = make([]ValidatorUpdate, v32) + for i := 0; i < v32; i++ { + v33 := NewPopulatedValidatorUpdate(r, easy) + this.ValidatorUpdates[i] = *v33 } } if r.Intn(10) != 0 { this.ConsensusParamUpdates = NewPopulatedConsensusParams(r, easy) } if r.Intn(10) != 0 { - v28 := r.Intn(5) - this.Events = make([]Event, v28) - for i := 0; i < v28; i++ { - v29 := NewPopulatedEvent(r, easy) - this.Events[i] = *v29 + v34 := r.Intn(5) + this.Events = make([]Event, v34) + for i := 0; i < v34; i++ { + v35 := NewPopulatedEvent(r, easy) + this.Events[i] = *v35 } } if !easy && r.Intn(10) != 0 { @@ -8267,9 +8793,9 @@ func NewPopulatedResponseEndBlock(r randyTypes, easy bool) *ResponseEndBlock { func NewPopulatedResponseCommit(r randyTypes, easy bool) *ResponseCommit { this := &ResponseCommit{} - v30 := r.Intn(100) - this.Data = make([]byte, v30) - for i := 0; i < v30; i++ { + v36 := r.Intn(100) + this.Data = make([]byte, v36) + for i := 0; i < v36; i++ { this.Data[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8325,9 +8851,9 @@ func NewPopulatedEvidenceParams(r randyTypes, easy bool) *EvidenceParams { func NewPopulatedValidatorParams(r randyTypes, easy bool) *ValidatorParams { this := &ValidatorParams{} - v31 := r.Intn(10) - this.PubKeyTypes = make([]string, v31) - for i := 0; i < v31; i++ { + v37 := r.Intn(10) + this.PubKeyTypes = make([]string, v37) + for i := 0; i < v37; i++ { this.PubKeyTypes[i] = string(randStringTypes(r)) } if !easy && r.Intn(10) != 0 { @@ -8343,11 +8869,11 @@ func NewPopulatedLastCommitInfo(r randyTypes, easy bool) *LastCommitInfo { this.Round *= -1 } if r.Intn(10) != 0 { - v32 := r.Intn(5) - this.Votes = make([]VoteInfo, v32) - for i := 0; i < v32; i++ { - v33 := NewPopulatedVoteInfo(r, easy) - this.Votes[i] = *v33 + v38 := r.Intn(5) + this.Votes = make([]VoteInfo, v38) + for i := 0; i < v38; i++ { + v39 := NewPopulatedVoteInfo(r, easy) + this.Votes[i] = *v39 } } if !easy && r.Intn(10) != 0 { @@ -8360,11 +8886,11 @@ func NewPopulatedEvent(r randyTypes, easy bool) *Event { this := &Event{} this.Type = string(randStringTypes(r)) if r.Intn(10) != 0 { - v34 := r.Intn(5) - this.Attributes = make([]common.KVPair, v34) - for i := 0; i < v34; i++ { - v35 := common.NewPopulatedKVPair(r, easy) - this.Attributes[i] = *v35 + v40 := r.Intn(5) + this.Attributes = make([]common.KVPair, v40) + for i := 0; i < v40; i++ { + v41 := common.NewPopulatedKVPair(r, easy) + this.Attributes[i] = *v41 } } if !easy && r.Intn(10) != 0 { @@ -8375,15 +8901,15 @@ func NewPopulatedEvent(r randyTypes, easy bool) *Event { func NewPopulatedHeader(r randyTypes, easy bool) *Header { this := &Header{} - v36 := NewPopulatedVersion(r, easy) - this.Version = *v36 + v42 := NewPopulatedVersion(r, easy) + this.Version = *v42 this.ChainID = string(randStringTypes(r)) this.Height = int64(r.Int63()) if r.Intn(2) == 0 { this.Height *= -1 } - v37 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v37 + v43 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v43 this.NumTxs = int64(r.Int63()) if r.Intn(2) == 0 { this.NumTxs *= -1 @@ -8392,51 +8918,51 @@ func NewPopulatedHeader(r randyTypes, easy bool) *Header { if r.Intn(2) == 0 { this.TotalTxs *= -1 } - v38 := NewPopulatedBlockID(r, easy) - this.LastBlockId = *v38 - v39 := r.Intn(100) - this.LastCommitHash = make([]byte, v39) - for i := 0; i < v39; i++ { + v44 := NewPopulatedBlockID(r, easy) + this.LastBlockId = *v44 + v45 := r.Intn(100) + this.LastCommitHash = make([]byte, v45) + for i := 0; i < v45; i++ { this.LastCommitHash[i] = byte(r.Intn(256)) } - v40 := r.Intn(100) - this.DataHash = make([]byte, v40) - for i := 0; i < v40; i++ { + v46 := r.Intn(100) + this.DataHash = make([]byte, v46) + for i := 0; i < v46; i++ { this.DataHash[i] = byte(r.Intn(256)) } - v41 := r.Intn(100) - this.ValidatorsHash = make([]byte, v41) - for i := 0; i < v41; i++ { + v47 := r.Intn(100) + this.ValidatorsHash = make([]byte, v47) + for i := 0; i < v47; i++ { this.ValidatorsHash[i] = byte(r.Intn(256)) } - v42 := r.Intn(100) - this.NextValidatorsHash = make([]byte, v42) - for i := 0; i < v42; i++ { + v48 := r.Intn(100) + this.NextValidatorsHash = make([]byte, v48) + for i := 0; i < v48; i++ { this.NextValidatorsHash[i] = byte(r.Intn(256)) } - v43 := r.Intn(100) - this.ConsensusHash = make([]byte, v43) - for i := 0; i < v43; i++ { + v49 := r.Intn(100) + this.ConsensusHash = make([]byte, v49) + for i := 0; i < v49; i++ { this.ConsensusHash[i] = byte(r.Intn(256)) } - v44 := r.Intn(100) - this.AppHash = make([]byte, v44) - for i := 0; i < v44; i++ { + v50 := r.Intn(100) + this.AppHash = make([]byte, v50) + for i := 0; i < v50; i++ { this.AppHash[i] = byte(r.Intn(256)) } - v45 := r.Intn(100) - this.LastResultsHash = make([]byte, v45) - for i := 0; i < v45; i++ { + v51 := r.Intn(100) + this.LastResultsHash = make([]byte, v51) + for i := 0; i < v51; i++ { this.LastResultsHash[i] = byte(r.Intn(256)) } - v46 := r.Intn(100) - this.EvidenceHash = make([]byte, v46) - for i := 0; i < v46; i++ { + v52 := r.Intn(100) + this.EvidenceHash = make([]byte, v52) + for i := 0; i < v52; i++ { this.EvidenceHash[i] = byte(r.Intn(256)) } - v47 := r.Intn(100) - this.ProposerAddress = make([]byte, v47) - for i := 0; i < v47; i++ { + v53 := r.Intn(100) + this.ProposerAddress = make([]byte, v53) + for i := 0; i < v53; i++ { this.ProposerAddress[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8457,13 +8983,13 @@ func NewPopulatedVersion(r randyTypes, easy bool) *Version { func NewPopulatedBlockID(r randyTypes, easy bool) *BlockID { this := &BlockID{} - v48 := r.Intn(100) - this.Hash = make([]byte, v48) - for i := 0; i < v48; i++ { + v54 := r.Intn(100) + this.Hash = make([]byte, v54) + for i := 0; i < v54; i++ { this.Hash[i] = byte(r.Intn(256)) } - v49 := NewPopulatedPartSetHeader(r, easy) - this.PartsHeader = *v49 + v55 := NewPopulatedPartSetHeader(r, easy) + this.PartsHeader = *v55 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedTypes(r, 3) } @@ -8476,9 +9002,9 @@ func NewPopulatedPartSetHeader(r randyTypes, easy bool) *PartSetHeader { if r.Intn(2) == 0 { this.Total *= -1 } - v50 := r.Intn(100) - this.Hash = make([]byte, v50) - for i := 0; i < v50; i++ { + v56 := r.Intn(100) + this.Hash = make([]byte, v56) + for i := 0; i < v56; i++ { this.Hash[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8489,9 +9015,9 @@ func NewPopulatedPartSetHeader(r randyTypes, easy bool) *PartSetHeader { func NewPopulatedValidator(r randyTypes, easy bool) *Validator { this := &Validator{} - v51 := r.Intn(100) - this.Address = make([]byte, v51) - for i := 0; i < v51; i++ { + v57 := r.Intn(100) + this.Address = make([]byte, v57) + for i := 0; i < v57; i++ { this.Address[i] = byte(r.Intn(256)) } this.Power = int64(r.Int63()) @@ -8506,8 +9032,8 @@ func NewPopulatedValidator(r randyTypes, easy bool) *Validator { func NewPopulatedValidatorUpdate(r randyTypes, easy bool) *ValidatorUpdate { this := &ValidatorUpdate{} - v52 := NewPopulatedPubKey(r, easy) - this.PubKey = *v52 + v58 := NewPopulatedPubKey(r, easy) + this.PubKey = *v58 this.Power = int64(r.Int63()) if r.Intn(2) == 0 { this.Power *= -1 @@ -8520,8 +9046,8 @@ func NewPopulatedValidatorUpdate(r randyTypes, easy bool) *ValidatorUpdate { func NewPopulatedVoteInfo(r randyTypes, easy bool) *VoteInfo { this := &VoteInfo{} - v53 := NewPopulatedValidator(r, easy) - this.Validator = *v53 + v59 := NewPopulatedValidator(r, easy) + this.Validator = *v59 this.SignedLastBlock = bool(bool(r.Intn(2) == 0)) if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedTypes(r, 3) @@ -8532,9 +9058,9 @@ func NewPopulatedVoteInfo(r randyTypes, easy bool) *VoteInfo { func NewPopulatedPubKey(r randyTypes, easy bool) *PubKey { this := &PubKey{} this.Type = string(randStringTypes(r)) - v54 := r.Intn(100) - this.Data = make([]byte, v54) - for i := 0; i < v54; i++ { + v60 := r.Intn(100) + this.Data = make([]byte, v60) + for i := 0; i < v60; i++ { this.Data[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8546,14 +9072,14 @@ func NewPopulatedPubKey(r randyTypes, easy bool) *PubKey { func NewPopulatedEvidence(r randyTypes, easy bool) *Evidence { this := &Evidence{} this.Type = string(randStringTypes(r)) - v55 := NewPopulatedValidator(r, easy) - this.Validator = *v55 + v61 := NewPopulatedValidator(r, easy) + this.Validator = *v61 this.Height = int64(r.Int63()) if r.Intn(2) == 0 { this.Height *= -1 } - v56 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v56 + v62 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v62 this.TotalVotingPower = int64(r.Int63()) if r.Intn(2) == 0 { this.TotalVotingPower *= -1 @@ -8583,9 +9109,9 @@ func randUTF8RuneTypes(r randyTypes) rune { return rune(ru + 61) } func randStringTypes(r randyTypes) string { - v57 := r.Intn(100) - tmps := make([]rune, v57) - for i := 0; i < v57; i++ { + v63 := r.Intn(100) + tmps := make([]rune, v63) + for i := 0; i < v63; i++ { tmps[i] = randUTF8RuneTypes(r) } return string(tmps) @@ -8607,11 +9133,11 @@ func randFieldTypes(dAtA []byte, r randyTypes, fieldNumber int, wire int) []byte switch wire { case 0: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) - v58 := r.Int63() + v64 := r.Int63() if r.Intn(2) == 0 { - v58 *= -1 + v64 *= -1 } - dAtA = encodeVarintPopulateTypes(dAtA, uint64(v58)) + dAtA = encodeVarintPopulateTypes(dAtA, uint64(v64)) case 1: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) dAtA = append(dAtA, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256))) @@ -9383,7 +9909,7 @@ func (m *ResponseCheckTx) Size() (n int) { return n } -func (m *ResponseDeliverTx) Size() (n int) { +func (m *ResponseCheckTxDeprecated) Size() (n int) { if m == nil { return 0 } @@ -9410,8 +9936,8 @@ func (m *ResponseDeliverTx) Size() (n int) { if m.GasUsed != 0 { n += 1 + sovTypes(uint64(m.GasUsed)) } - if len(m.Events) > 0 { - for _, e := range m.Events { + if len(m.Tags) > 0 { + for _, e := range m.Tags { l = e.Size() n += 1 + l + sovTypes(uint64(l)) } @@ -9426,20 +9952,106 @@ func (m *ResponseDeliverTx) Size() (n int) { return n } -func (m *ResponseEndBlock) Size() (n int) { +func (m *ResponseDeliverTx) Size() (n int) { if m == nil { return 0 } var l int _ = l - if len(m.ValidatorUpdates) > 0 { - for _, e := range m.ValidatorUpdates { - l = e.Size() - n += 1 + l + sovTypes(uint64(l)) - } + if m.Code != 0 { + n += 1 + sovTypes(uint64(m.Code)) } - if m.ConsensusParamUpdates != nil { - l = m.ConsensusParamUpdates.Size() + l = len(m.Data) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + l = len(m.Log) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + l = len(m.Info) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + if m.GasWanted != 0 { + n += 1 + sovTypes(uint64(m.GasWanted)) + } + if m.GasUsed != 0 { + n += 1 + sovTypes(uint64(m.GasUsed)) + } + if len(m.Events) > 0 { + for _, e := range m.Events { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + l = len(m.Codespace) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *ResponseDeliverTxDeprecated) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if m.Code != 0 { + n += 1 + sovTypes(uint64(m.Code)) + } + l = len(m.Data) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + l = len(m.Log) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + l = len(m.Info) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + if m.GasWanted != 0 { + n += 1 + sovTypes(uint64(m.GasWanted)) + } + if m.GasUsed != 0 { + n += 1 + sovTypes(uint64(m.GasUsed)) + } + if len(m.Tags) > 0 { + for _, e := range m.Tags { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + l = len(m.Codespace) + if l > 0 { + n += 1 + l + sovTypes(uint64(l)) + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + +func (m *ResponseEndBlock) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.ValidatorUpdates) > 0 { + for _, e := range m.ValidatorUpdates { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if m.ConsensusParamUpdates != nil { + l = m.ConsensusParamUpdates.Size() n += 1 + l + sovTypes(uint64(l)) } if len(m.Events) > 0 { @@ -13111,7 +13723,7 @@ func (m *ResponseCheckTx) Unmarshal(dAtA []byte) error { } return nil } -func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { +func (m *ResponseCheckTxDeprecated) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 for iNdEx < l { @@ -13134,10 +13746,10 @@ func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { fieldNum := int32(wire >> 3) wireType := int(wire & 0x7) if wireType == 4 { - return fmt.Errorf("proto: ResponseDeliverTx: wiretype end group for non-group") + return fmt.Errorf("proto: ResponseCheckTxDeprecated: wiretype end group for non-group") } if fieldNum <= 0 { - return fmt.Errorf("proto: ResponseDeliverTx: illegal tag %d (wire type %d)", fieldNum, wire) + return fmt.Errorf("proto: ResponseCheckTxDeprecated: illegal tag %d (wire type %d)", fieldNum, wire) } switch fieldNum { case 1: @@ -13288,7 +13900,7 @@ func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { } case 7: if wireType != 2 { - return fmt.Errorf("proto: wrong wireType = %d for field Events", wireType) + return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) } var msglen int for shift := uint(0); ; shift += 7 { @@ -13312,8 +13924,522 @@ func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { if postIndex > l { return io.ErrUnexpectedEOF } - m.Events = append(m.Events, Event{}) - if err := m.Events[len(m.Events)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + m.Tags = append(m.Tags, common.KVPair{}) + if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Codespace", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Codespace = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ResponseDeliverTx) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResponseDeliverTx: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResponseDeliverTx: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Code", wireType) + } + m.Code = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Code |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + byteLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) + if m.Data == nil { + m.Data = []byte{} + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Log", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Log = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Info", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Info = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field GasWanted", wireType) + } + m.GasWanted = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.GasWanted |= (int64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field GasUsed", wireType) + } + m.GasUsed = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.GasUsed |= (int64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Events", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Events = append(m.Events, Event{}) + if err := m.Events[len(m.Events)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 8: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Codespace", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Codespace = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} +func (m *ResponseDeliverTxDeprecated) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResponseDeliverTxDeprecated: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResponseDeliverTxDeprecated: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field Code", wireType) + } + m.Code = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.Code |= (uint32(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Data", wireType) + } + var byteLen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + byteLen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if byteLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + byteLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Data = append(m.Data[:0], dAtA[iNdEx:postIndex]...) + if m.Data == nil { + m.Data = []byte{} + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Log", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Log = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 4: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Info", wireType) + } + var stringLen uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + stringLen |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + intStringLen := int(stringLen) + if intStringLen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + intStringLen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Info = string(dAtA[iNdEx:postIndex]) + iNdEx = postIndex + case 5: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field GasWanted", wireType) + } + m.GasWanted = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.GasWanted |= (int64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 6: + if wireType != 0 { + return fmt.Errorf("proto: wrong wireType = %d for field GasUsed", wireType) + } + m.GasUsed = 0 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + m.GasUsed |= (int64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + case 7: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Tags = append(m.Tags, common.KVPair{}) + if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { return err } iNdEx = postIndex @@ -15701,155 +16827,158 @@ var ( ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") ) -func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_b4cfdaa127d201bd) } +func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7fd6485bbc04efd7) } func init() { - golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_b4cfdaa127d201bd) -} - -var fileDescriptor_types_b4cfdaa127d201bd = []byte{ - // 2291 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x58, 0xcd, 0x73, 0x1c, 0x57, - 0x11, 0xd7, 0xec, 0xf7, 0xf4, 0x6a, 0x3f, 0xfc, 0x2c, 0xdb, 0xeb, 0xc5, 0x48, 0xae, 0x31, 0x38, - 0x56, 0xe2, 0xac, 0x12, 0x05, 0x53, 0x32, 0x0e, 0xa9, 0xd2, 0xda, 0x26, 0x52, 0xc5, 0x04, 0x31, - 0xb6, 0xc5, 0x85, 0xaa, 0xa9, 0xb7, 0x3b, 0xcf, 0xbb, 0x53, 0xde, 0x9d, 0x99, 0xcc, 0xbc, 0x95, - 0x57, 0x3e, 0x72, 0xce, 0x21, 0x07, 0xfe, 0x08, 0xae, 0xdc, 0x72, 0xe4, 0x44, 0xe5, 0xc8, 0x81, - 0xb3, 0x01, 0x51, 0x5c, 0xa8, 0xe2, 0x0c, 0xdc, 0xa8, 0xd7, 0xef, 0xcd, 0xa7, 0x66, 0x4d, 0x62, - 0xb8, 0xe5, 0xb2, 0x3b, 0xef, 0x75, 0xf7, 0xfb, 0xe8, 0xd7, 0xdd, 0xbf, 0xee, 0x86, 0xcb, 0x74, - 0x34, 0x76, 0x76, 0xf8, 0xa9, 0xcf, 0x42, 0xf9, 0x3b, 0xf0, 0x03, 0x8f, 0x7b, 0xa4, 0x8a, 0x83, - 0xfe, 0xbb, 0x13, 0x87, 0x4f, 0x17, 0xa3, 0xc1, 0xd8, 0x9b, 0xef, 0x4c, 0xbc, 0x89, 0xb7, 0x83, - 0xd4, 0xd1, 0xe2, 0x19, 0x8e, 0x70, 0x80, 0x5f, 0x52, 0xaa, 0xbf, 0x35, 0xf1, 0xbc, 0xc9, 0x8c, - 0x25, 0x5c, 0xdc, 0x99, 0xb3, 0x90, 0xd3, 0xb9, 0xaf, 0x18, 0xf6, 0x52, 0xeb, 0x71, 0xe6, 0xda, - 0x2c, 0x98, 0x3b, 0x2e, 0x4f, 0x7f, 0xce, 0x9c, 0x51, 0xb8, 0x33, 0xf6, 0xe6, 0x73, 0xcf, 0x4d, - 0x1f, 0xa8, 0x7f, 0xef, 0xbf, 0x4a, 0x8e, 0x83, 0x53, 0x9f, 0x7b, 0x3b, 0x73, 0x16, 0x3c, 0x9f, - 0x31, 0xf5, 0x27, 0x85, 0x8d, 0xdf, 0x57, 0xa0, 0x6e, 0xb2, 0xcf, 0x16, 0x2c, 0xe4, 0xe4, 0x16, - 0x54, 0xd8, 0x78, 0xea, 0xf5, 0x4a, 0xd7, 0xb5, 0x5b, 0xcd, 0x5d, 0x32, 0x90, 0x9b, 0x28, 0xea, - 0xc3, 0xf1, 0xd4, 0x3b, 0x58, 0x33, 0x91, 0x83, 0xbc, 0x03, 0xd5, 0x67, 0xb3, 0x45, 0x38, 0xed, - 0x95, 0x91, 0xf5, 0x62, 0x96, 0xf5, 0x27, 0x82, 0x74, 0xb0, 0x66, 0x4a, 0x1e, 0xb1, 0xac, 0xe3, - 0x3e, 0xf3, 0x7a, 0x95, 0xa2, 0x65, 0x0f, 0xdd, 0x67, 0xb8, 0xac, 0xe0, 0x20, 0x7b, 0x00, 0x21, - 0xe3, 0x96, 0xe7, 0x73, 0xc7, 0x73, 0x7b, 0x55, 0xe4, 0xbf, 0x92, 0xe5, 0x7f, 0xcc, 0xf8, 0xcf, - 0x90, 0x7c, 0xb0, 0x66, 0xea, 0x61, 0x34, 0x10, 0x92, 0x8e, 0xeb, 0x70, 0x6b, 0x3c, 0xa5, 0x8e, - 0xdb, 0xab, 0x15, 0x49, 0x1e, 0xba, 0x0e, 0xbf, 0x2f, 0xc8, 0x42, 0xd2, 0x89, 0x06, 0xe2, 0x2a, - 0x9f, 0x2d, 0x58, 0x70, 0xda, 0xab, 0x17, 0x5d, 0xe5, 0xe7, 0x82, 0x24, 0xae, 0x82, 0x3c, 0xe4, - 0x1e, 0x34, 0x47, 0x6c, 0xe2, 0xb8, 0xd6, 0x68, 0xe6, 0x8d, 0x9f, 0xf7, 0x1a, 0x28, 0xd2, 0xcb, - 0x8a, 0x0c, 0x05, 0xc3, 0x50, 0xd0, 0x0f, 0xd6, 0x4c, 0x18, 0xc5, 0x23, 0xb2, 0x0b, 0x8d, 0xf1, - 0x94, 0x8d, 0x9f, 0x5b, 0x7c, 0xd9, 0xd3, 0x51, 0xf2, 0x52, 0x56, 0xf2, 0xbe, 0xa0, 0x3e, 0x59, - 0x1e, 0xac, 0x99, 0xf5, 0xb1, 0xfc, 0x24, 0x77, 0x40, 0x67, 0xae, 0xad, 0xb6, 0x6b, 0xa2, 0xd0, - 0xe5, 0xdc, 0xbb, 0xb8, 0x76, 0xb4, 0x59, 0x83, 0xa9, 0x6f, 0x32, 0x80, 0x9a, 0x30, 0x14, 0x87, - 0xf7, 0xd6, 0x51, 0x66, 0x23, 0xb7, 0x11, 0xd2, 0x0e, 0xd6, 0x4c, 0xc5, 0x25, 0xd4, 0x67, 0xb3, - 0x99, 0x73, 0xc2, 0x02, 0x71, 0xb8, 0x8b, 0x45, 0xea, 0x7b, 0x20, 0xe9, 0x78, 0x3c, 0xdd, 0x8e, - 0x06, 0xc3, 0x3a, 0x54, 0x4f, 0xe8, 0x6c, 0xc1, 0x8c, 0xb7, 0xa0, 0x99, 0xb2, 0x14, 0xd2, 0x83, - 0xfa, 0x9c, 0x85, 0x21, 0x9d, 0xb0, 0x9e, 0x76, 0x5d, 0xbb, 0xa5, 0x9b, 0xd1, 0xd0, 0x68, 0xc3, - 0x7a, 0xda, 0x4e, 0x8c, 0x79, 0x2c, 0x28, 0x6c, 0x41, 0x08, 0x9e, 0xb0, 0x20, 0x14, 0x06, 0xa0, - 0x04, 0xd5, 0x90, 0xdc, 0x80, 0x16, 0xea, 0xc1, 0x8a, 0xe8, 0xc2, 0x4e, 0x2b, 0xe6, 0x3a, 0x4e, - 0x1e, 0x2b, 0xa6, 0x2d, 0x68, 0xfa, 0xbb, 0x7e, 0xcc, 0x52, 0x46, 0x16, 0xf0, 0x77, 0x7d, 0xc5, - 0x60, 0xfc, 0x08, 0xba, 0x79, 0x53, 0x22, 0x5d, 0x28, 0x3f, 0x67, 0xa7, 0x6a, 0x3f, 0xf1, 0x49, - 0x36, 0xd4, 0xb5, 0x70, 0x0f, 0xdd, 0x54, 0x77, 0xfc, 0xa2, 0x14, 0x0b, 0xc7, 0xd6, 0x44, 0xf6, - 0xa0, 0x22, 0x7c, 0x19, 0xa5, 0x9b, 0xbb, 0xfd, 0x81, 0x74, 0xf4, 0x41, 0xe4, 0xe8, 0x83, 0x27, - 0x91, 0xa3, 0x0f, 0x1b, 0x5f, 0xbd, 0xda, 0x5a, 0xfb, 0xe2, 0x4f, 0x5b, 0x9a, 0x89, 0x12, 0xe4, - 0xaa, 0x30, 0x08, 0xea, 0xb8, 0x96, 0x63, 0xab, 0x7d, 0xea, 0x38, 0x3e, 0xb4, 0xc9, 0x3e, 0x74, - 0xc7, 0x9e, 0x1b, 0x32, 0x37, 0x5c, 0x84, 0x96, 0x4f, 0x03, 0x3a, 0x0f, 0x95, 0xaf, 0x45, 0xcf, - 0x7f, 0x3f, 0x22, 0x1f, 0x21, 0xd5, 0xec, 0x8c, 0xb3, 0x13, 0xe4, 0x43, 0x80, 0x13, 0x3a, 0x73, - 0x6c, 0xca, 0xbd, 0x20, 0xec, 0x55, 0xae, 0x97, 0x53, 0xc2, 0xc7, 0x11, 0xe1, 0xa9, 0x6f, 0x53, - 0xce, 0x86, 0x15, 0x71, 0x32, 0x33, 0xc5, 0x4f, 0x6e, 0x42, 0x87, 0xfa, 0xbe, 0x15, 0x72, 0xca, - 0x99, 0x35, 0x3a, 0xe5, 0x2c, 0x44, 0x7f, 0x5c, 0x37, 0x5b, 0xd4, 0xf7, 0x1f, 0x8b, 0xd9, 0xa1, - 0x98, 0x34, 0xec, 0xf8, 0x35, 0xd1, 0x55, 0x08, 0x81, 0x8a, 0x4d, 0x39, 0x45, 0x6d, 0xac, 0x9b, - 0xf8, 0x2d, 0xe6, 0x7c, 0xca, 0xa7, 0xea, 0x8e, 0xf8, 0x4d, 0x2e, 0x43, 0x6d, 0xca, 0x9c, 0xc9, - 0x94, 0xe3, 0xb5, 0xca, 0xa6, 0x1a, 0x09, 0xc5, 0xfb, 0x81, 0x77, 0xc2, 0x30, 0x5a, 0x34, 0x4c, - 0x39, 0x30, 0xfe, 0xa6, 0xc1, 0x85, 0x73, 0xee, 0x25, 0xd6, 0x9d, 0xd2, 0x70, 0x1a, 0xed, 0x25, - 0xbe, 0xc9, 0x3b, 0x62, 0x5d, 0x6a, 0xb3, 0x40, 0x45, 0xb1, 0x96, 0xba, 0xf1, 0x01, 0x4e, 0xaa, - 0x8b, 0x2a, 0x16, 0xf2, 0x10, 0xba, 0x33, 0x1a, 0x72, 0x4b, 0x7a, 0x81, 0x85, 0x51, 0xaa, 0x9c, - 0xf1, 0xcc, 0x47, 0x34, 0xf2, 0x16, 0x61, 0x9c, 0x4a, 0xbc, 0x3d, 0xcb, 0xcc, 0x92, 0x03, 0xd8, - 0x18, 0x9d, 0xbe, 0xa4, 0x2e, 0x77, 0x5c, 0x66, 0x9d, 0xd3, 0x79, 0x47, 0x2d, 0xf5, 0xf0, 0xc4, - 0xb1, 0x99, 0x3b, 0x8e, 0x94, 0x7d, 0x31, 0x16, 0x89, 0x1f, 0x23, 0x34, 0x0e, 0xa0, 0x9d, 0x8d, - 0x05, 0xa4, 0x0d, 0x25, 0xbe, 0x54, 0x37, 0x2c, 0xf1, 0x25, 0xb9, 0x09, 0x15, 0xb1, 0x1c, 0xde, - 0xae, 0x1d, 0x07, 0x53, 0xc5, 0xfd, 0xe4, 0xd4, 0x67, 0x26, 0xd2, 0x0d, 0x23, 0xb6, 0xd4, 0xd8, - 0x71, 0xf3, 0x6b, 0x19, 0xdb, 0xd0, 0xc9, 0x05, 0x91, 0xd4, 0xb3, 0x68, 0xe9, 0x67, 0x31, 0x3a, - 0xd0, 0xca, 0xc4, 0x0e, 0xe3, 0xf3, 0x2a, 0x34, 0x4c, 0x16, 0xfa, 0xc2, 0xe8, 0xc8, 0x1e, 0xe8, - 0x6c, 0x39, 0x66, 0x32, 0x6c, 0x6b, 0xb9, 0xa0, 0x28, 0x79, 0x1e, 0x46, 0x74, 0x11, 0x3e, 0x62, - 0x66, 0xb2, 0x9d, 0x81, 0x9c, 0x8b, 0x79, 0xa1, 0x34, 0xe6, 0xdc, 0xce, 0x62, 0xce, 0x46, 0x8e, - 0x37, 0x07, 0x3a, 0xdb, 0x19, 0xd0, 0xc9, 0x2f, 0x9c, 0x41, 0x9d, 0xbb, 0x05, 0xa8, 0x93, 0x3f, - 0xfe, 0x0a, 0xd8, 0xb9, 0x5b, 0x00, 0x3b, 0xbd, 0x73, 0x7b, 0x15, 0xe2, 0xce, 0xed, 0x2c, 0xee, - 0xe4, 0xaf, 0x93, 0x03, 0x9e, 0x0f, 0x8b, 0x80, 0xe7, 0x6a, 0x4e, 0x66, 0x25, 0xf2, 0x7c, 0x70, - 0x0e, 0x79, 0x2e, 0xe7, 0x44, 0x0b, 0xa0, 0xe7, 0x6e, 0x06, 0x13, 0xa0, 0xf0, 0x6e, 0xc5, 0xa0, - 0x40, 0x7e, 0x78, 0x1e, 0xb5, 0xae, 0xe4, 0x9f, 0xb6, 0x08, 0xb6, 0x76, 0x72, 0xb0, 0x75, 0x29, - 0x7f, 0xca, 0x1c, 0x6e, 0x25, 0xe8, 0xb3, 0x2d, 0xe2, 0x43, 0xce, 0xd2, 0x44, 0x2c, 0x61, 0x41, - 0xe0, 0x05, 0x2a, 0xb0, 0xcb, 0x81, 0x71, 0x4b, 0x44, 0xac, 0xc4, 0xbe, 0x5e, 0x83, 0x54, 0x68, - 0xf4, 0x29, 0xeb, 0x32, 0xbe, 0xd4, 0x12, 0x59, 0xf4, 0xfc, 0x74, 0xb4, 0xd3, 0x55, 0xb4, 0x4b, - 0x01, 0x58, 0x29, 0x0b, 0x60, 0x5b, 0xd0, 0x14, 0x31, 0x35, 0x87, 0x4d, 0xd4, 0x8f, 0xb0, 0x89, - 0xbc, 0x0d, 0x17, 0x30, 0x1e, 0x49, 0x98, 0x53, 0x8e, 0x58, 0x41, 0x47, 0xec, 0x08, 0x82, 0xd4, - 0x98, 0x0c, 0x94, 0xef, 0xc2, 0xc5, 0x14, 0xaf, 0x58, 0x17, 0x63, 0xa1, 0x0c, 0xd2, 0xdd, 0x98, - 0x7b, 0xdf, 0xf7, 0x0f, 0x68, 0x38, 0x35, 0x7e, 0x9a, 0x28, 0x28, 0xc1, 0x3d, 0x02, 0x95, 0xb1, - 0x67, 0xcb, 0x7b, 0xb7, 0x4c, 0xfc, 0x16, 0x58, 0x38, 0xf3, 0x26, 0x78, 0x38, 0xdd, 0x14, 0x9f, - 0x82, 0x2b, 0x76, 0x25, 0x5d, 0xfa, 0x8c, 0xf1, 0x6b, 0x2d, 0x59, 0x2f, 0x81, 0xc2, 0x22, 0xd4, - 0xd2, 0xfe, 0x17, 0xd4, 0x2a, 0x7d, 0x33, 0xd4, 0x32, 0xce, 0xb4, 0xe4, 0xc9, 0x62, 0x3c, 0x7a, - 0xb3, 0x2b, 0x0a, 0xeb, 0x71, 0x5c, 0x9b, 0x2d, 0x51, 0xa5, 0x65, 0x53, 0x0e, 0xa2, 0x54, 0xa1, - 0x86, 0x6a, 0xce, 0xa6, 0x0a, 0x75, 0x9c, 0x93, 0x03, 0x72, 0x03, 0x71, 0xcc, 0x7b, 0xa6, 0x5c, - 0xb5, 0x35, 0x50, 0x59, 0xf7, 0x91, 0x98, 0x34, 0x25, 0x2d, 0x15, 0x6d, 0xf5, 0x0c, 0x08, 0x5e, - 0x03, 0x5d, 0x1c, 0x34, 0xf4, 0xe9, 0x98, 0xa1, 0xe7, 0xe9, 0x66, 0x32, 0x61, 0x3c, 0x01, 0x72, - 0xde, 0xe3, 0xc9, 0x47, 0x50, 0x63, 0x27, 0xcc, 0xe5, 0x42, 0xe3, 0x42, 0x69, 0xeb, 0x31, 0xec, - 0x30, 0x97, 0x0f, 0x7b, 0x42, 0x55, 0x7f, 0x7f, 0xb5, 0xd5, 0x95, 0x3c, 0xb7, 0xbd, 0xb9, 0xc3, - 0xd9, 0xdc, 0xe7, 0xa7, 0xa6, 0x92, 0x32, 0xfe, 0xa9, 0x09, 0x34, 0xc8, 0x44, 0x83, 0x42, 0xe5, - 0x45, 0x26, 0x5f, 0x4a, 0x01, 0xfc, 0xd7, 0x53, 0xe8, 0x77, 0x01, 0x26, 0x34, 0xb4, 0x5e, 0x50, - 0x97, 0x33, 0x5b, 0x69, 0x55, 0x9f, 0xd0, 0xf0, 0x17, 0x38, 0x21, 0xb2, 0x21, 0x41, 0x5e, 0x84, - 0xcc, 0x46, 0xf5, 0x96, 0xcd, 0xfa, 0x84, 0x86, 0x4f, 0x43, 0x66, 0xa7, 0xee, 0x56, 0x7f, 0x93, - 0xbb, 0x65, 0xf5, 0xd9, 0xc8, 0xeb, 0xf3, 0xdf, 0x29, 0x5b, 0x4e, 0xc0, 0xf2, 0xdb, 0x71, 0xf7, - 0x7f, 0x68, 0x22, 0x4f, 0xc8, 0x86, 0x64, 0x72, 0x08, 0x17, 0x62, 0x9f, 0xb2, 0x16, 0xe8, 0x6b, - 0x91, 0x55, 0xbd, 0xde, 0x15, 0xbb, 0x27, 0xd9, 0xe9, 0x90, 0x7c, 0x0a, 0x57, 0x72, 0x11, 0x21, - 0x5e, 0xb0, 0xf4, 0xda, 0xc0, 0x70, 0x29, 0x1b, 0x18, 0xa2, 0xf5, 0x12, 0x6d, 0x94, 0xdf, 0xc8, - 0xca, 0xbf, 0x27, 0x12, 0xac, 0x34, 0x98, 0x14, 0xbd, 0xa9, 0xf1, 0x5b, 0x0d, 0x3a, 0xb9, 0x03, - 0x91, 0x3b, 0x00, 0x32, 0xd4, 0x86, 0xce, 0x4b, 0x96, 0x8b, 0x6a, 0xa8, 0xb6, 0xc7, 0xce, 0x4b, - 0xa6, 0x0e, 0xaf, 0x8f, 0xa2, 0x09, 0xf2, 0x3e, 0x34, 0x98, 0x4a, 0xfc, 0xd4, 0x8d, 0x2f, 0xe5, - 0xf2, 0x41, 0x25, 0x13, 0xb3, 0x91, 0x1f, 0x80, 0x1e, 0xeb, 0x31, 0x97, 0xf4, 0xc7, 0x6a, 0x8f, - 0x36, 0x8a, 0x19, 0x8d, 0x8f, 0xa1, 0x93, 0x3b, 0x06, 0xf9, 0x0e, 0xe8, 0x73, 0xba, 0x54, 0xd9, - 0xbb, 0xcc, 0xe7, 0x1a, 0x73, 0xba, 0xc4, 0xc4, 0x9d, 0x5c, 0x81, 0xba, 0x20, 0x4e, 0xa8, 0x7c, - 0x89, 0xb2, 0x59, 0x9b, 0xd3, 0xe5, 0xc7, 0x34, 0x34, 0xb6, 0xa1, 0x9d, 0x3d, 0x5a, 0xc4, 0x1a, - 0x21, 0xa4, 0x64, 0xdd, 0x9f, 0x30, 0xe3, 0x0e, 0x74, 0x72, 0x27, 0x22, 0x06, 0xb4, 0xfc, 0xc5, - 0xc8, 0x7a, 0xce, 0x4e, 0x2d, 0x3c, 0x32, 0xda, 0x8d, 0x6e, 0x36, 0xfd, 0xc5, 0xe8, 0x13, 0x76, - 0x2a, 0x12, 0xd4, 0xd0, 0x78, 0x0c, 0xed, 0x6c, 0x5e, 0x2d, 0x62, 0x68, 0xe0, 0x2d, 0x5c, 0x1b, - 0xd7, 0xaf, 0x9a, 0x72, 0x20, 0x4a, 0xf3, 0x13, 0x4f, 0x9a, 0x4a, 0x3a, 0x91, 0x3e, 0xf6, 0x38, - 0x4b, 0x65, 0xe3, 0x92, 0xc7, 0x70, 0xa0, 0x8a, 0x46, 0x20, 0x1e, 0x14, 0x33, 0x64, 0x85, 0xc9, - 0xe2, 0x9b, 0x3c, 0x02, 0xa0, 0x9c, 0x07, 0xce, 0x68, 0x91, 0x2c, 0xd7, 0x1e, 0xc8, 0x5e, 0xca, - 0xe0, 0x93, 0xe3, 0x23, 0xea, 0x04, 0xc3, 0x6b, 0xca, 0x78, 0x36, 0x12, 0xce, 0x94, 0x01, 0xa5, - 0xe4, 0x8d, 0x5f, 0x55, 0xa1, 0x26, 0xeb, 0x09, 0x32, 0xc8, 0x56, 0xab, 0x62, 0x55, 0x75, 0x48, - 0x39, 0xab, 0xce, 0x18, 0xa7, 0x00, 0x37, 0xf3, 0x25, 0xdf, 0xb0, 0x79, 0xf6, 0x6a, 0xab, 0x8e, - 0xf0, 0x79, 0xf8, 0x20, 0xa9, 0xff, 0x56, 0x95, 0x47, 0x51, 0xb1, 0x59, 0xf9, 0xc6, 0xc5, 0xe6, - 0x15, 0xa8, 0xbb, 0x8b, 0xb9, 0xc5, 0x97, 0xa1, 0x0a, 0x3f, 0x35, 0x77, 0x31, 0x7f, 0xb2, 0x44, - 0x2b, 0xe1, 0x1e, 0xa7, 0x33, 0x24, 0xc9, 0xe0, 0xd3, 0xc0, 0x09, 0x41, 0xdc, 0x83, 0x56, 0x2a, - 0xcb, 0x70, 0x6c, 0x95, 0xad, 0xb6, 0xd3, 0x86, 0x7f, 0xf8, 0x40, 0xdd, 0xb2, 0x19, 0x67, 0x1d, - 0x87, 0x36, 0xb9, 0x95, 0xad, 0xad, 0x30, 0x39, 0x69, 0xa0, 0x8f, 0xa5, 0xca, 0x27, 0x91, 0x9a, - 0x88, 0x03, 0x08, 0xaf, 0x93, 0x2c, 0x3a, 0xb2, 0x34, 0xc4, 0x04, 0x12, 0xdf, 0x82, 0x4e, 0x82, - 0xef, 0x92, 0x05, 0xe4, 0x2a, 0xc9, 0x34, 0x32, 0xbe, 0x07, 0x1b, 0x2e, 0x5b, 0x72, 0x2b, 0xcf, - 0xdd, 0x44, 0x6e, 0x22, 0x68, 0xc7, 0x59, 0x89, 0xef, 0x43, 0x3b, 0x89, 0x4d, 0xc8, 0xbb, 0x2e, - 0x2b, 0xdc, 0x78, 0x16, 0xd9, 0xae, 0x42, 0x23, 0xce, 0xae, 0x5a, 0xc8, 0x50, 0xa7, 0x32, 0xa9, - 0x8a, 0xf3, 0xb5, 0x80, 0x85, 0x8b, 0x19, 0x57, 0x8b, 0xb4, 0x91, 0x07, 0xf3, 0x35, 0x53, 0xce, - 0x23, 0xef, 0x0d, 0x68, 0x45, 0x1e, 0x2e, 0xf9, 0x3a, 0xc8, 0xb7, 0x1e, 0x4d, 0x22, 0xd3, 0x36, - 0x74, 0xfd, 0xc0, 0xf3, 0xbd, 0x90, 0x05, 0x16, 0xb5, 0xed, 0x80, 0x85, 0x61, 0xaf, 0x2b, 0xd7, - 0x8b, 0xe6, 0xf7, 0xe5, 0xb4, 0xf1, 0x3e, 0xd4, 0xa3, 0xb4, 0x71, 0x03, 0xaa, 0xa8, 0x75, 0x34, - 0xc1, 0x8a, 0x29, 0x07, 0x02, 0x98, 0xf6, 0x7d, 0x5f, 0x35, 0x49, 0xc4, 0xa7, 0xf1, 0x4b, 0xa8, - 0xab, 0x07, 0x2b, 0x2c, 0x9d, 0x7f, 0x0c, 0xeb, 0x3e, 0x0d, 0xc4, 0x35, 0xd2, 0x05, 0x74, 0x54, - 0x98, 0x1c, 0xd1, 0x80, 0x3f, 0x66, 0x3c, 0x53, 0x47, 0x37, 0x91, 0x5f, 0x4e, 0x19, 0x77, 0xa1, - 0x95, 0xe1, 0x11, 0xc7, 0x42, 0x3b, 0x8a, 0x9c, 0x1a, 0x07, 0xf1, 0xce, 0xa5, 0x64, 0x67, 0xe3, - 0x1e, 0xe8, 0xf1, 0xdb, 0x88, 0xfc, 0x39, 0xba, 0xba, 0xa6, 0xd4, 0x2d, 0x87, 0xd8, 0x1b, 0xf0, - 0x5e, 0xb0, 0x40, 0xf9, 0x84, 0x1c, 0x18, 0x4f, 0x53, 0x41, 0x48, 0xc2, 0x04, 0xb9, 0x0d, 0x75, - 0x15, 0x84, 0x94, 0x57, 0x46, 0x5d, 0x80, 0x23, 0x8c, 0x42, 0x51, 0x17, 0x40, 0xc6, 0xa4, 0x64, - 0xd9, 0x52, 0x7a, 0xd9, 0x19, 0x34, 0xa2, 0x40, 0x93, 0x8d, 0xc8, 0x72, 0xc5, 0x6e, 0x3e, 0x22, - 0xab, 0x45, 0x13, 0x46, 0x61, 0x1d, 0xa1, 0x33, 0x71, 0x99, 0x6d, 0x25, 0x2e, 0x84, 0x7b, 0x34, - 0xcc, 0x8e, 0x24, 0x3c, 0x8a, 0xfc, 0xc5, 0x78, 0x0f, 0x6a, 0xf2, 0x6c, 0x85, 0xe1, 0xab, 0x08, - 0xa3, 0xfe, 0xa8, 0x41, 0x23, 0x8a, 0xd3, 0x85, 0x42, 0x99, 0x43, 0x97, 0xbe, 0xee, 0xa1, 0xff, - 0xff, 0x81, 0xe7, 0x36, 0x10, 0x19, 0x5f, 0x4e, 0x3c, 0xee, 0xb8, 0x13, 0x4b, 0xea, 0x5a, 0xc6, - 0xa0, 0x2e, 0x52, 0x8e, 0x91, 0x70, 0x24, 0xe6, 0xdf, 0xbe, 0x01, 0xcd, 0x54, 0x33, 0x83, 0xd4, - 0xa1, 0xfc, 0x29, 0x7b, 0xd1, 0x5d, 0x23, 0x4d, 0xa8, 0x9b, 0x0c, 0x4b, 0xd3, 0xae, 0xb6, 0xfb, - 0x79, 0x15, 0x3a, 0xfb, 0xc3, 0xfb, 0x87, 0xfb, 0xbe, 0x3f, 0x73, 0xc6, 0x14, 0x6b, 0x99, 0x1d, - 0xa8, 0x60, 0x39, 0x57, 0xd0, 0xb6, 0xee, 0x17, 0xf5, 0x15, 0xc8, 0x2e, 0x54, 0xb1, 0xaa, 0x23, - 0x45, 0xdd, 0xeb, 0x7e, 0x61, 0x7b, 0x41, 0x6c, 0x22, 0xeb, 0xbe, 0xf3, 0x4d, 0xec, 0x7e, 0x51, - 0x8f, 0x81, 0x7c, 0x04, 0x7a, 0x52, 0x6e, 0xad, 0x6a, 0x65, 0xf7, 0x57, 0x76, 0x1b, 0x84, 0x7c, - 0x92, 0x92, 0xae, 0xea, 0xc8, 0xf6, 0x57, 0x96, 0xe5, 0x64, 0x0f, 0xea, 0x51, 0x32, 0x5f, 0xdc, - 0x6c, 0xee, 0xaf, 0xe8, 0x04, 0x08, 0xf5, 0xc8, 0x0a, 0xaa, 0xa8, 0x23, 0xde, 0x2f, 0x6c, 0x57, - 0x90, 0x3b, 0x50, 0x53, 0x59, 0x55, 0x61, 0xc3, 0xb9, 0x5f, 0x5c, 0xcf, 0x8b, 0x4b, 0x26, 0x35, - 0xe4, 0xaa, 0xae, 0x7d, 0x7f, 0x65, 0x5f, 0x85, 0xec, 0x03, 0xa4, 0x0a, 0xa1, 0x95, 0xed, 0xf8, - 0xfe, 0xea, 0x7e, 0x09, 0xb9, 0x07, 0x8d, 0xa4, 0x07, 0x56, 0xdc, 0x60, 0xef, 0xaf, 0x6a, 0x61, - 0x0c, 0xaf, 0xfd, 0xeb, 0x2f, 0x9b, 0xda, 0x6f, 0xce, 0x36, 0xb5, 0x2f, 0xcf, 0x36, 0xb5, 0xaf, - 0xce, 0x36, 0xb5, 0x3f, 0x9c, 0x6d, 0x6a, 0x7f, 0x3e, 0xdb, 0xd4, 0x7e, 0xf7, 0xd7, 0x4d, 0x6d, - 0x54, 0x43, 0x1f, 0xf9, 0xe0, 0x3f, 0x01, 0x00, 0x00, 0xff, 0xff, 0xee, 0x92, 0xae, 0xde, 0x50, - 0x1a, 0x00, 0x00, + golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7fd6485bbc04efd7) +} + +var fileDescriptor_types_7fd6485bbc04efd7 = []byte{ + // 2350 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x59, 0xcd, 0x73, 0x1c, 0x57, + 0x11, 0xd7, 0x68, 0x3f, 0xa7, 0x57, 0xfb, 0xe1, 0x67, 0xd9, 0x5e, 0x6f, 0x8c, 0xe4, 0x1a, 0x83, + 0x63, 0xc5, 0xce, 0x2a, 0x51, 0x30, 0x25, 0xc7, 0x21, 0x55, 0x5a, 0xdb, 0x44, 0xaa, 0x98, 0x20, + 0xc6, 0xb6, 0xb8, 0x50, 0x35, 0xf5, 0x76, 0xe7, 0x79, 0x77, 0xca, 0xbb, 0x33, 0x93, 0x99, 0xb7, + 0xf2, 0xca, 0x47, 0xce, 0x39, 0xe4, 0xc0, 0x1f, 0xc1, 0x95, 0x5b, 0x8e, 0x9c, 0xa8, 0x1c, 0x39, + 0x70, 0x36, 0x20, 0x8a, 0x0b, 0x55, 0x9c, 0x81, 0x03, 0x05, 0xf5, 0xfa, 0xbd, 0xf9, 0xd4, 0xac, + 0x93, 0x18, 0x0e, 0x54, 0xe5, 0x22, 0xcd, 0x7b, 0xdd, 0xfd, 0x3e, 0xfa, 0x75, 0xf7, 0xaf, 0xbb, + 0x17, 0x2e, 0xd2, 0xe1, 0xc8, 0xd9, 0xe6, 0x27, 0x3e, 0x0b, 0xe5, 0xdf, 0xbe, 0x1f, 0x78, 0xdc, + 0x23, 0x15, 0x1c, 0xf4, 0xde, 0x1e, 0x3b, 0x7c, 0x32, 0x1f, 0xf6, 0x47, 0xde, 0x6c, 0x7b, 0xec, + 0x8d, 0xbd, 0x6d, 0xa4, 0x0e, 0xe7, 0x4f, 0x71, 0x84, 0x03, 0xfc, 0x92, 0x52, 0xbd, 0xcd, 0xb1, + 0xe7, 0x8d, 0xa7, 0x2c, 0xe1, 0xe2, 0xce, 0x8c, 0x85, 0x9c, 0xce, 0x7c, 0xc5, 0xb0, 0x9b, 0x5a, + 0x8f, 0x33, 0xd7, 0x66, 0xc1, 0xcc, 0x71, 0x79, 0xfa, 0x73, 0xea, 0x0c, 0xc3, 0xed, 0x91, 0x37, + 0x9b, 0x79, 0x6e, 0xfa, 0x40, 0xbd, 0xbb, 0x5f, 0x29, 0x39, 0x0a, 0x4e, 0x7c, 0xee, 0x6d, 0xcf, + 0x58, 0xf0, 0x6c, 0xca, 0xd4, 0x3f, 0x29, 0x6c, 0xfc, 0xb6, 0x0c, 0x35, 0x93, 0x7d, 0x3a, 0x67, + 0x21, 0x27, 0x37, 0xa0, 0xcc, 0x46, 0x13, 0xaf, 0xbb, 0x7a, 0x55, 0xbb, 0xd1, 0xd8, 0x21, 0x7d, + 0xb9, 0x89, 0xa2, 0x3e, 0x18, 0x4d, 0xbc, 0xfd, 0x15, 0x13, 0x39, 0xc8, 0x4d, 0xa8, 0x3c, 0x9d, + 0xce, 0xc3, 0x49, 0xb7, 0x84, 0xac, 0xe7, 0xb3, 0xac, 0x3f, 0x12, 0xa4, 0xfd, 0x15, 0x53, 0xf2, + 0x88, 0x65, 0x1d, 0xf7, 0xa9, 0xd7, 0x2d, 0x17, 0x2d, 0x7b, 0xe0, 0x3e, 0xc5, 0x65, 0x05, 0x07, + 0xd9, 0x05, 0x08, 0x19, 0xb7, 0x3c, 0x9f, 0x3b, 0x9e, 0xdb, 0xad, 0x20, 0xff, 0xa5, 0x2c, 0xff, + 0x23, 0xc6, 0x7f, 0x82, 0xe4, 0xfd, 0x15, 0x53, 0x0f, 0xa3, 0x81, 0x90, 0x74, 0x5c, 0x87, 0x5b, + 0xa3, 0x09, 0x75, 0xdc, 0x6e, 0xb5, 0x48, 0xf2, 0xc0, 0x75, 0xf8, 0x3d, 0x41, 0x16, 0x92, 0x4e, + 0x34, 0x10, 0x57, 0xf9, 0x74, 0xce, 0x82, 0x93, 0x6e, 0xad, 0xe8, 0x2a, 0x3f, 0x15, 0x24, 0x71, + 0x15, 0xe4, 0x21, 0x77, 0xa1, 0x31, 0x64, 0x63, 0xc7, 0xb5, 0x86, 0x53, 0x6f, 0xf4, 0xac, 0x5b, + 0x47, 0x91, 0x6e, 0x56, 0x64, 0x20, 0x18, 0x06, 0x82, 0xbe, 0xbf, 0x62, 0xc2, 0x30, 0x1e, 0x91, + 0x1d, 0xa8, 0x8f, 0x26, 0x6c, 0xf4, 0xcc, 0xe2, 0x8b, 0xae, 0x8e, 0x92, 0x17, 0xb2, 0x92, 0xf7, + 0x04, 0xf5, 0xf1, 0x62, 0x7f, 0xc5, 0xac, 0x8d, 0xe4, 0x27, 0xb9, 0x0d, 0x3a, 0x73, 0x6d, 0xb5, + 0x5d, 0x03, 0x85, 0x2e, 0xe6, 0xde, 0xc5, 0xb5, 0xa3, 0xcd, 0xea, 0x4c, 0x7d, 0x93, 0x3e, 0x54, + 0x85, 0xa1, 0x38, 0xbc, 0xbb, 0x86, 0x32, 0xeb, 0xb9, 0x8d, 0x90, 0xb6, 0xbf, 0x62, 0x2a, 0x2e, + 0xa1, 0x3e, 0x9b, 0x4d, 0x9d, 0x63, 0x16, 0x88, 0xc3, 0x9d, 0x2f, 0x52, 0xdf, 0x7d, 0x49, 0xc7, + 0xe3, 0xe9, 0x76, 0x34, 0x18, 0xd4, 0xa0, 0x72, 0x4c, 0xa7, 0x73, 0x66, 0xbc, 0x09, 0x8d, 0x94, + 0xa5, 0x90, 0x2e, 0xd4, 0x66, 0x2c, 0x0c, 0xe9, 0x98, 0x75, 0xb5, 0xab, 0xda, 0x0d, 0xdd, 0x8c, + 0x86, 0x46, 0x0b, 0xd6, 0xd2, 0x76, 0x62, 0xcc, 0x62, 0x41, 0x61, 0x0b, 0x42, 0xf0, 0x98, 0x05, + 0xa1, 0x30, 0x00, 0x25, 0xa8, 0x86, 0xe4, 0x1a, 0x34, 0x51, 0x0f, 0x56, 0x44, 0x17, 0x76, 0x5a, + 0x36, 0xd7, 0x70, 0xf2, 0x48, 0x31, 0x6d, 0x42, 0xc3, 0xdf, 0xf1, 0x63, 0x96, 0x12, 0xb2, 0x80, + 0xbf, 0xe3, 0x2b, 0x06, 0xe3, 0x7d, 0xe8, 0xe4, 0x4d, 0x89, 0x74, 0xa0, 0xf4, 0x8c, 0x9d, 0xa8, + 0xfd, 0xc4, 0x27, 0x59, 0x57, 0xd7, 0xc2, 0x3d, 0x74, 0x53, 0xdd, 0xf1, 0xf3, 0xd5, 0x58, 0x38, + 0xb6, 0x26, 0xb2, 0x0b, 0x65, 0xe1, 0xcb, 0x28, 0xdd, 0xd8, 0xe9, 0xf5, 0xa5, 0xa3, 0xf7, 0x23, + 0x47, 0xef, 0x3f, 0x8e, 0x1c, 0x7d, 0x50, 0xff, 0xf2, 0xe5, 0xe6, 0xca, 0xe7, 0x7f, 0xd8, 0xd4, + 0x4c, 0x94, 0x20, 0x97, 0x85, 0x41, 0x50, 0xc7, 0xb5, 0x1c, 0x5b, 0xed, 0x53, 0xc3, 0xf1, 0x81, + 0x4d, 0xf6, 0xa0, 0x33, 0xf2, 0xdc, 0x90, 0xb9, 0xe1, 0x3c, 0xb4, 0x7c, 0x1a, 0xd0, 0x59, 0xa8, + 0x7c, 0x2d, 0x7a, 0xfe, 0x7b, 0x11, 0xf9, 0x10, 0xa9, 0x66, 0x7b, 0x94, 0x9d, 0x20, 0x1f, 0x00, + 0x1c, 0xd3, 0xa9, 0x63, 0x53, 0xee, 0x05, 0x61, 0xb7, 0x7c, 0xb5, 0x94, 0x12, 0x3e, 0x8a, 0x08, + 0x4f, 0x7c, 0x9b, 0x72, 0x36, 0x28, 0x8b, 0x93, 0x99, 0x29, 0x7e, 0x72, 0x1d, 0xda, 0xd4, 0xf7, + 0xad, 0x90, 0x53, 0xce, 0xac, 0xe1, 0x09, 0x67, 0x21, 0xfa, 0xe3, 0x9a, 0xd9, 0xa4, 0xbe, 0xff, + 0x48, 0xcc, 0x0e, 0xc4, 0xa4, 0x61, 0xc7, 0xaf, 0x89, 0xae, 0x42, 0x08, 0x94, 0x6d, 0xca, 0x29, + 0x6a, 0x63, 0xcd, 0xc4, 0x6f, 0x31, 0xe7, 0x53, 0x3e, 0x51, 0x77, 0xc4, 0x6f, 0x72, 0x11, 0xaa, + 0x13, 0xe6, 0x8c, 0x27, 0x1c, 0xaf, 0x55, 0x32, 0xd5, 0x48, 0x28, 0xde, 0x0f, 0xbc, 0x63, 0x86, + 0xd1, 0xa2, 0x6e, 0xca, 0x81, 0xf1, 0x17, 0x0d, 0xce, 0x9d, 0x71, 0x2f, 0xb1, 0xee, 0x84, 0x86, + 0x93, 0x68, 0x2f, 0xf1, 0x4d, 0x6e, 0x8a, 0x75, 0xa9, 0xcd, 0x02, 0x15, 0xc5, 0x9a, 0xea, 0xc6, + 0xfb, 0x38, 0xa9, 0x2e, 0xaa, 0x58, 0xc8, 0x03, 0xe8, 0x4c, 0x69, 0xc8, 0x2d, 0xe9, 0x05, 0x16, + 0x46, 0xa9, 0x52, 0xc6, 0x33, 0x1f, 0xd2, 0xc8, 0x5b, 0x84, 0x71, 0x2a, 0xf1, 0xd6, 0x34, 0x33, + 0x4b, 0xf6, 0x61, 0x7d, 0x78, 0xf2, 0x82, 0xba, 0xdc, 0x71, 0x99, 0x75, 0x46, 0xe7, 0x6d, 0xb5, + 0xd4, 0x83, 0x63, 0xc7, 0x66, 0xee, 0x28, 0x52, 0xf6, 0xf9, 0x58, 0x24, 0x7e, 0x8c, 0xd0, 0xd8, + 0x87, 0x56, 0x36, 0x16, 0x90, 0x16, 0xac, 0xf2, 0x85, 0xba, 0xe1, 0x2a, 0x5f, 0x90, 0xeb, 0x50, + 0x16, 0xcb, 0xe1, 0xed, 0x5a, 0x71, 0x30, 0x55, 0xdc, 0x8f, 0x4f, 0x7c, 0x66, 0x22, 0xdd, 0x30, + 0x62, 0x4b, 0x8d, 0x1d, 0x37, 0xbf, 0x96, 0xb1, 0x05, 0xed, 0x5c, 0x10, 0x49, 0x3d, 0x8b, 0x96, + 0x7e, 0x16, 0xa3, 0x0d, 0xcd, 0x4c, 0xec, 0x30, 0x3e, 0xab, 0x40, 0xdd, 0x64, 0xa1, 0x2f, 0x8c, + 0x8e, 0xec, 0x82, 0xce, 0x16, 0x23, 0x26, 0xc3, 0xb6, 0x96, 0x0b, 0x8a, 0x92, 0xe7, 0x41, 0x44, + 0x17, 0xe1, 0x23, 0x66, 0x26, 0x5b, 0x19, 0xc8, 0x39, 0x9f, 0x17, 0x4a, 0x63, 0xce, 0xad, 0x2c, + 0xe6, 0xac, 0xe7, 0x78, 0x73, 0xa0, 0xb3, 0x95, 0x01, 0x9d, 0xfc, 0xc2, 0x19, 0xd4, 0xb9, 0x53, + 0x80, 0x3a, 0xf9, 0xe3, 0x2f, 0x81, 0x9d, 0x3b, 0x05, 0xb0, 0xd3, 0x3d, 0xb3, 0x57, 0x21, 0xee, + 0xdc, 0xca, 0xe2, 0x4e, 0xfe, 0x3a, 0x39, 0xe0, 0xf9, 0xa0, 0x08, 0x78, 0x2e, 0xe7, 0x64, 0x96, + 0x22, 0xcf, 0x7b, 0x67, 0x90, 0xe7, 0x62, 0x4e, 0xb4, 0x00, 0x7a, 0xee, 0x64, 0x30, 0x01, 0x0a, + 0xef, 0x56, 0x0c, 0x0a, 0xe4, 0x07, 0x67, 0x51, 0xeb, 0x52, 0xfe, 0x69, 0x8b, 0x60, 0x6b, 0x3b, + 0x07, 0x5b, 0x17, 0xf2, 0xa7, 0xcc, 0xe1, 0x56, 0x82, 0x3e, 0x5b, 0x22, 0x3e, 0xe4, 0x2c, 0x4d, + 0xc4, 0x12, 0x16, 0x04, 0x5e, 0xa0, 0x02, 0xbb, 0x1c, 0x18, 0x37, 0x44, 0xc4, 0x4a, 0xec, 0xeb, + 0x15, 0x48, 0x85, 0x46, 0x9f, 0xb2, 0x2e, 0xe3, 0x0b, 0x2d, 0x91, 0x45, 0xcf, 0x4f, 0x47, 0x3b, + 0x5d, 0x45, 0xbb, 0x14, 0x80, 0xad, 0x66, 0x01, 0x6c, 0x13, 0x1a, 0x22, 0xa6, 0xe6, 0xb0, 0x89, + 0xfa, 0x11, 0x36, 0x91, 0xb7, 0xe0, 0x1c, 0xc6, 0x23, 0x09, 0x73, 0xca, 0x11, 0xcb, 0xe8, 0x88, + 0x6d, 0x41, 0x90, 0x1a, 0x93, 0x81, 0xf2, 0x6d, 0x38, 0x9f, 0xe2, 0x15, 0xeb, 0x62, 0x2c, 0x94, + 0x41, 0xba, 0x13, 0x73, 0xef, 0xf9, 0xfe, 0x3e, 0x0d, 0x27, 0xc6, 0x8f, 0x13, 0x05, 0x25, 0xb8, + 0x47, 0xa0, 0x3c, 0xf2, 0x6c, 0x79, 0xef, 0xa6, 0x89, 0xdf, 0x02, 0x0b, 0xa7, 0xde, 0x18, 0x0f, + 0xa7, 0x9b, 0xe2, 0x53, 0x70, 0xc5, 0xae, 0xa4, 0x4b, 0x9f, 0x31, 0x7e, 0xa9, 0x25, 0xeb, 0x25, + 0x50, 0x58, 0x84, 0x5a, 0xda, 0x7f, 0x83, 0x5a, 0xab, 0xdf, 0x0c, 0xb5, 0x8c, 0x53, 0x2d, 0x79, + 0xb2, 0x18, 0x8f, 0x5e, 0xef, 0x8a, 0xc2, 0x7a, 0x1c, 0xd7, 0x66, 0x0b, 0x54, 0x69, 0xc9, 0x94, + 0x83, 0x28, 0x55, 0xa8, 0xa2, 0x9a, 0xb3, 0xa9, 0x42, 0x0d, 0xe7, 0xe4, 0x80, 0x5c, 0x43, 0x1c, + 0xf3, 0x9e, 0x2a, 0x57, 0x6d, 0xf6, 0x55, 0xd6, 0x7d, 0x28, 0x26, 0x4d, 0x49, 0x4b, 0x45, 0x5b, + 0x3d, 0x03, 0x82, 0x57, 0x40, 0x17, 0x07, 0x0d, 0x7d, 0x3a, 0x62, 0xe8, 0x79, 0xba, 0x99, 0x4c, + 0x18, 0x8f, 0x81, 0x9c, 0xf5, 0x78, 0xf2, 0x21, 0x54, 0xd9, 0x31, 0x73, 0xb9, 0xd0, 0xb8, 0x50, + 0xda, 0x5a, 0x0c, 0x3b, 0xcc, 0xe5, 0x83, 0xae, 0x50, 0xd5, 0x5f, 0x5f, 0x6e, 0x76, 0x24, 0xcf, + 0x2d, 0x6f, 0xe6, 0x70, 0x36, 0xf3, 0xf9, 0x89, 0xa9, 0xa4, 0x8c, 0xbf, 0x6b, 0x02, 0x0d, 0x32, + 0xd1, 0xa0, 0x50, 0x79, 0x91, 0xc9, 0xaf, 0xa6, 0x00, 0xfe, 0xeb, 0x29, 0xf4, 0x3b, 0x00, 0x63, + 0x1a, 0x5a, 0xcf, 0xa9, 0xcb, 0x99, 0xad, 0xb4, 0xaa, 0x8f, 0x69, 0xf8, 0x33, 0x9c, 0x10, 0xd9, + 0x90, 0x20, 0xcf, 0x43, 0x66, 0xa3, 0x7a, 0x4b, 0x66, 0x6d, 0x4c, 0xc3, 0x27, 0x21, 0xb3, 0x53, + 0x77, 0xab, 0xbd, 0xce, 0xdd, 0xb2, 0xfa, 0xac, 0xe7, 0xf5, 0xf9, 0x2f, 0x0d, 0x2e, 0xe7, 0x6e, + 0x7e, 0x9f, 0xf9, 0x01, 0x1b, 0x51, 0x71, 0xac, 0xff, 0x17, 0x1d, 0xbc, 0x0f, 0x65, 0x4e, 0xc7, + 0x91, 0x06, 0x5a, 0x7d, 0x59, 0x08, 0xf6, 0x3f, 0x3e, 0x3a, 0xa4, 0x4e, 0x30, 0xb8, 0xa8, 0x74, + 0xd0, 0x12, 0x3c, 0x29, 0x0d, 0xa0, 0xcc, 0x57, 0xdc, 0xff, 0x9f, 0x29, 0x5f, 0x4e, 0x92, 0x85, + 0x6f, 0xc7, 0xdb, 0xff, 0x5b, 0x83, 0x37, 0xce, 0xdc, 0xfd, 0x5b, 0xf5, 0xfa, 0x7f, 0xd3, 0x44, + 0xa6, 0x98, 0x05, 0x65, 0x72, 0x00, 0xe7, 0xe2, 0xa8, 0x6a, 0xcd, 0x31, 0xda, 0x46, 0x71, 0xe5, + 0xd5, 0xc1, 0xb8, 0x73, 0x9c, 0x9d, 0x0e, 0xc9, 0x27, 0x70, 0x29, 0x87, 0x09, 0xf1, 0x82, 0xab, + 0xaf, 0x84, 0x86, 0x0b, 0x59, 0x68, 0x88, 0xd6, 0x4b, 0xec, 0xa1, 0xf4, 0x5a, 0x71, 0xee, 0xbb, + 0x22, 0xc5, 0x4e, 0xa7, 0x13, 0x45, 0xef, 0x69, 0xfc, 0x5a, 0x83, 0x76, 0xee, 0x40, 0xe4, 0x36, + 0x80, 0x04, 0xdb, 0xd0, 0x79, 0xc1, 0x72, 0xb8, 0x86, 0x6a, 0x7b, 0xe4, 0xbc, 0x60, 0xea, 0xf0, + 0xfa, 0x30, 0x9a, 0x20, 0xef, 0x42, 0x9d, 0xa9, 0xd4, 0x5f, 0xdd, 0xf8, 0x42, 0xae, 0x22, 0x50, + 0x32, 0x31, 0x1b, 0xf9, 0x3e, 0xe8, 0xb1, 0x1e, 0x73, 0x65, 0x5f, 0xac, 0xf6, 0x68, 0xa3, 0x98, + 0xd1, 0xf8, 0x08, 0xda, 0xb9, 0x63, 0x90, 0x37, 0x40, 0x9f, 0xd1, 0x85, 0xaa, 0xdf, 0x64, 0x46, + 0x5f, 0x9f, 0xd1, 0x05, 0x96, 0x6e, 0xe4, 0x12, 0xd4, 0x04, 0x71, 0x4c, 0xe5, 0x4b, 0x94, 0xcc, + 0xea, 0x8c, 0x2e, 0x3e, 0xa2, 0xa1, 0xb1, 0x05, 0xad, 0xec, 0xd1, 0x22, 0xd6, 0x28, 0x47, 0x92, + 0xac, 0x7b, 0x63, 0x66, 0xdc, 0x86, 0x76, 0xee, 0x44, 0xc4, 0x80, 0xa6, 0x3f, 0x1f, 0x5a, 0xcf, + 0xd8, 0x89, 0x85, 0x47, 0x46, 0xbb, 0xd1, 0xcd, 0x86, 0x3f, 0x1f, 0x7e, 0xcc, 0x4e, 0x44, 0x89, + 0x12, 0x1a, 0x8f, 0xa0, 0x95, 0xad, 0xac, 0x04, 0x8a, 0x06, 0xde, 0xdc, 0xb5, 0x71, 0xfd, 0x8a, + 0x29, 0x07, 0xe4, 0x26, 0x54, 0x8e, 0x3d, 0x69, 0x2a, 0xe9, 0x52, 0xea, 0xc8, 0xe3, 0x2c, 0x55, + 0x8f, 0x49, 0x1e, 0xc3, 0x81, 0x0a, 0x1a, 0x81, 0x78, 0x50, 0xac, 0x91, 0x54, 0x56, 0x26, 0xbe, + 0xc9, 0x43, 0x00, 0xca, 0x79, 0xe0, 0x0c, 0xe7, 0xc9, 0x72, 0x79, 0x37, 0xba, 0xa2, 0x8c, 0x67, + 0x3d, 0xe1, 0x4c, 0x19, 0x50, 0x4a, 0xde, 0xf8, 0x45, 0x05, 0xaa, 0xb2, 0xa2, 0x24, 0xfd, 0x6c, + 0xbf, 0x42, 0xac, 0xaa, 0x0e, 0x29, 0x67, 0xd5, 0x19, 0xe3, 0x24, 0xf0, 0x7a, 0xbe, 0xe8, 0x1f, + 0x34, 0x4e, 0x5f, 0x6e, 0xd6, 0x30, 0x81, 0x3a, 0xb8, 0x9f, 0x74, 0x00, 0x96, 0x15, 0xc8, 0x51, + 0xbb, 0xa1, 0xfc, 0x8d, 0xdb, 0x0d, 0x97, 0xa0, 0xe6, 0xce, 0x67, 0x16, 0x5f, 0x84, 0x2a, 0xf4, + 0x54, 0xdd, 0xf9, 0xec, 0xf1, 0x02, 0xad, 0x84, 0x7b, 0x9c, 0x4e, 0x91, 0x24, 0x03, 0x4f, 0x1d, + 0x27, 0x04, 0x71, 0x17, 0x9a, 0xa9, 0x3c, 0xd3, 0xb1, 0x55, 0xbd, 0xd2, 0x4a, 0x1b, 0xfe, 0xc1, + 0x7d, 0x75, 0xcb, 0x46, 0x9c, 0x77, 0x1e, 0xd8, 0xe4, 0x46, 0xb6, 0xba, 0xc6, 0xf4, 0xb4, 0x8e, + 0x3e, 0x96, 0x2a, 0xa0, 0x45, 0x72, 0x2a, 0x0e, 0x20, 0xbc, 0x4e, 0xb2, 0xe8, 0xc8, 0x52, 0x17, + 0x13, 0x48, 0x7c, 0x13, 0xda, 0x49, 0x86, 0x27, 0x59, 0x40, 0xae, 0x92, 0x4c, 0x23, 0xe3, 0x3b, + 0xb0, 0xee, 0xb2, 0x05, 0xb7, 0xf2, 0xdc, 0x0d, 0xe4, 0x26, 0x82, 0x76, 0x94, 0x95, 0xf8, 0x1e, + 0xb4, 0x92, 0xd8, 0x84, 0xbc, 0x6b, 0xb2, 0xc7, 0x11, 0xcf, 0x22, 0xdb, 0x65, 0xa8, 0xc7, 0xf9, + 0x75, 0x13, 0x19, 0x6a, 0x54, 0xa6, 0xd5, 0x71, 0xc6, 0x1e, 0xb0, 0x70, 0x3e, 0xe5, 0x6a, 0x91, + 0x16, 0xf2, 0x60, 0xc6, 0x6e, 0xca, 0x79, 0xe4, 0xbd, 0x06, 0xcd, 0xc8, 0xc3, 0x25, 0x5f, 0x1b, + 0xf9, 0xd6, 0xa2, 0x49, 0x64, 0xda, 0x82, 0x8e, 0x1f, 0x78, 0xbe, 0x17, 0xb2, 0xc0, 0xa2, 0xb6, + 0x1d, 0xb0, 0x30, 0xec, 0x76, 0xe4, 0x7a, 0xd1, 0xfc, 0x9e, 0x9c, 0x36, 0xde, 0x85, 0x5a, 0x54, + 0x38, 0xac, 0x43, 0x05, 0xb5, 0x8e, 0x26, 0x58, 0x36, 0xe5, 0x40, 0x80, 0xd2, 0x9e, 0xef, 0xab, + 0x36, 0x99, 0xf8, 0x34, 0x7e, 0x0e, 0x35, 0xf5, 0x60, 0x85, 0xcd, 0x93, 0x1f, 0xc2, 0x9a, 0x4f, + 0x03, 0x71, 0x8d, 0x74, 0x0b, 0x25, 0x2a, 0x4d, 0x0f, 0x69, 0xc0, 0x1f, 0x31, 0x9e, 0xe9, 0xa4, + 0x34, 0x90, 0x5f, 0x4e, 0x19, 0x77, 0xa0, 0x99, 0xe1, 0x11, 0xc7, 0x42, 0x3b, 0x8a, 0x9c, 0x1a, + 0x07, 0xf1, 0xce, 0xab, 0xc9, 0xce, 0xc6, 0x5d, 0xd0, 0xe3, 0xb7, 0x11, 0x15, 0x54, 0x74, 0x75, + 0x4d, 0xa9, 0x5b, 0x0e, 0xb1, 0x3b, 0xe4, 0x3d, 0x67, 0x81, 0xf2, 0x09, 0x39, 0x30, 0x9e, 0xa4, + 0x82, 0x90, 0x84, 0x09, 0x72, 0x0b, 0x6a, 0x2a, 0x08, 0x29, 0xaf, 0x8c, 0xfa, 0x40, 0x87, 0x18, + 0x85, 0xa2, 0x3e, 0x90, 0x8c, 0x49, 0xc9, 0xb2, 0xab, 0xe9, 0x65, 0xa7, 0x50, 0x8f, 0x02, 0x4d, + 0x36, 0x22, 0xcb, 0x15, 0x3b, 0xf9, 0x88, 0xac, 0x16, 0x4d, 0x18, 0x85, 0x75, 0x84, 0xce, 0xd8, + 0x65, 0xb6, 0x95, 0xb8, 0x10, 0xee, 0x51, 0x37, 0xdb, 0x92, 0xf0, 0x30, 0xf2, 0x17, 0xe3, 0x1d, + 0xa8, 0xca, 0xb3, 0x15, 0x86, 0xaf, 0x22, 0x8c, 0xfa, 0xbd, 0x06, 0xf5, 0x28, 0x4e, 0x17, 0x0a, + 0x65, 0x0e, 0xbd, 0xfa, 0x75, 0x0f, 0xfd, 0xbf, 0x0f, 0x3c, 0xb7, 0x80, 0xc8, 0xf8, 0x72, 0xec, + 0x71, 0xc7, 0x1d, 0x5b, 0x52, 0xd7, 0x32, 0x06, 0x75, 0x90, 0x72, 0x84, 0x84, 0x43, 0x31, 0xff, + 0xd6, 0x35, 0x68, 0xa4, 0xda, 0x59, 0xa4, 0x06, 0xa5, 0x4f, 0xd8, 0xf3, 0xce, 0x0a, 0x69, 0x40, + 0xcd, 0x64, 0xd8, 0x9c, 0xe8, 0x68, 0x3b, 0x9f, 0x55, 0xa0, 0xbd, 0x37, 0xb8, 0x77, 0xb0, 0xe7, + 0xfb, 0x53, 0x67, 0x44, 0xb1, 0x9a, 0xdd, 0x86, 0x32, 0x16, 0xf4, 0x05, 0x3f, 0x5c, 0xf4, 0x8a, + 0x3a, 0x4b, 0x64, 0x07, 0x2a, 0x58, 0xd7, 0x93, 0xa2, 0xdf, 0x2f, 0x7a, 0x85, 0x0d, 0x26, 0xb1, + 0x89, 0xac, 0xfc, 0xcf, 0xfe, 0x8c, 0xd1, 0x2b, 0xea, 0x32, 0x91, 0x0f, 0x41, 0x4f, 0x0a, 0xee, + 0x65, 0x3f, 0x66, 0xf4, 0x96, 0xf6, 0x9b, 0x84, 0x7c, 0x92, 0x94, 0x2f, 0xeb, 0xc9, 0xf7, 0x96, + 0x36, 0x66, 0xc8, 0x2e, 0xd4, 0xa2, 0x72, 0xae, 0xf8, 0xe7, 0x86, 0xde, 0x92, 0x5e, 0x90, 0x50, + 0x8f, 0xac, 0xa1, 0x8b, 0x7e, 0x13, 0xe9, 0x15, 0x36, 0xac, 0xc8, 0x6d, 0xa8, 0xaa, 0xac, 0xaa, + 0xf0, 0x27, 0x87, 0x5e, 0x71, 0x47, 0x47, 0x5c, 0x32, 0xe9, 0x22, 0x2c, 0xfb, 0xdd, 0xa6, 0xb7, + 0xb4, 0xb3, 0x46, 0xf6, 0x00, 0x52, 0xa5, 0xf0, 0xd2, 0x1f, 0x64, 0x7a, 0xcb, 0x3b, 0x66, 0xe4, + 0x2e, 0xd4, 0x93, 0x2e, 0x68, 0xf1, 0x4f, 0x2c, 0xbd, 0x65, 0x4d, 0xac, 0xc1, 0x95, 0x7f, 0xfc, + 0x69, 0x43, 0xfb, 0xd5, 0xe9, 0x86, 0xf6, 0xc5, 0xe9, 0x86, 0xf6, 0xe5, 0xe9, 0x86, 0xf6, 0xbb, + 0xd3, 0x0d, 0xed, 0x8f, 0xa7, 0x1b, 0xda, 0x6f, 0xfe, 0xbc, 0xa1, 0x0d, 0xab, 0xe8, 0x23, 0xef, + 0xfd, 0x27, 0x00, 0x00, 0xff, 0xff, 0xad, 0x6a, 0xd3, 0xce, 0x52, 0x1c, 0x00, 0x00, } diff --git a/abci/types/types.proto b/abci/types/types.proto index 961b1360a..19d177ebb 100644 --- a/abci/types/types.proto +++ b/abci/types/types.proto @@ -185,6 +185,17 @@ message ResponseCheckTx { string codespace = 8; } +message ResponseCheckTxDeprecated { + uint32 code = 1; + bytes data = 2; + string log = 3; // nondeterministic + string info = 4; // nondeterministic + int64 gas_wanted = 5; + int64 gas_used = 6; + repeated common.KVPair tags = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; + string codespace = 8; +} + message ResponseDeliverTx { uint32 code = 1; bytes data = 2; @@ -196,6 +207,17 @@ message ResponseDeliverTx { string codespace = 8; } +message ResponseDeliverTxDeprecated { + uint32 code = 1; + bytes data = 2; + string log = 3; // nondeterministic + string info = 4; // nondeterministic + int64 gas_wanted = 5; + int64 gas_used = 6; + repeated common.KVPair tags = 7 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; + string codespace = 8; +} + message ResponseEndBlock { repeated ValidatorUpdate validator_updates = 1 [(gogoproto.nullable)=false]; ConsensusParams consensus_param_updates = 2; diff --git a/abci/types/typespb_test.go b/abci/types/typespb_test.go index 367393b7e..e86e626e9 100644 --- a/abci/types/typespb_test.go +++ b/abci/types/typespb_test.go @@ -1255,6 +1255,62 @@ func TestResponseCheckTxMarshalTo(t *testing.T) { } } +func TestResponseCheckTxDeprecatedProto(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseCheckTxDeprecated(popr, false) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseCheckTxDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + littlefuzz := make([]byte, len(dAtA)) + copy(littlefuzz, dAtA) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } + if len(littlefuzz) > 0 { + fuzzamount := 100 + for i := 0; i < fuzzamount; i++ { + littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256)) + littlefuzz = append(littlefuzz, byte(popr.Intn(256))) + } + // shouldn't panic + _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg) + } +} + +func TestResponseCheckTxDeprecatedMarshalTo(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseCheckTxDeprecated(popr, false) + size := p.Size() + dAtA := make([]byte, size) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + _, err := p.MarshalTo(dAtA) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseCheckTxDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseDeliverTxProto(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -1311,6 +1367,62 @@ func TestResponseDeliverTxMarshalTo(t *testing.T) { } } +func TestResponseDeliverTxDeprecatedProto(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseDeliverTxDeprecated(popr, false) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseDeliverTxDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + littlefuzz := make([]byte, len(dAtA)) + copy(littlefuzz, dAtA) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } + if len(littlefuzz) > 0 { + fuzzamount := 100 + for i := 0; i < fuzzamount; i++ { + littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256)) + littlefuzz = append(littlefuzz, byte(popr.Intn(256))) + } + // shouldn't panic + _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg) + } +} + +func TestResponseDeliverTxDeprecatedMarshalTo(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseDeliverTxDeprecated(popr, false) + size := p.Size() + dAtA := make([]byte, size) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + _, err := p.MarshalTo(dAtA) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseDeliverTxDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseEndBlockProto(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -2659,6 +2771,24 @@ func TestResponseCheckTxJSON(t *testing.T) { t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) } } +func TestResponseCheckTxDeprecatedJSON(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseCheckTxDeprecated(popr, true) + marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{} + jsondata, err := marshaler.MarshalToString(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseCheckTxDeprecated{} + err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) + } +} func TestResponseDeliverTxJSON(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -2677,6 +2807,24 @@ func TestResponseDeliverTxJSON(t *testing.T) { t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) } } +func TestResponseDeliverTxDeprecatedJSON(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseDeliverTxDeprecated(popr, true) + marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{} + jsondata, err := marshaler.MarshalToString(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseDeliverTxDeprecated{} + err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) + } +} func TestResponseEndBlockJSON(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -3599,6 +3747,34 @@ func TestResponseCheckTxProtoCompactText(t *testing.T) { } } +func TestResponseCheckTxDeprecatedProtoText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseCheckTxDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p) + msg := &ResponseCheckTxDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + +func TestResponseCheckTxDeprecatedProtoCompactText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseCheckTxDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.CompactTextString(p) + msg := &ResponseCheckTxDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseDeliverTxProtoText(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -3627,6 +3803,34 @@ func TestResponseDeliverTxProtoCompactText(t *testing.T) { } } +func TestResponseDeliverTxDeprecatedProtoText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseDeliverTxDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p) + msg := &ResponseDeliverTxDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + +func TestResponseDeliverTxDeprecatedProtoCompactText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseDeliverTxDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.CompactTextString(p) + msg := &ResponseDeliverTxDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseEndBlockProtoText(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -4587,6 +4791,28 @@ func TestResponseCheckTxSize(t *testing.T) { } } +func TestResponseCheckTxDeprecatedSize(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseCheckTxDeprecated(popr, true) + size2 := github_com_gogo_protobuf_proto.Size(p) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + size := p.Size() + if len(dAtA) != size { + t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA)) + } + if size2 != size { + t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2) + } + size3 := github_com_gogo_protobuf_proto.Size(p) + if size3 != size { + t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3) + } +} + func TestResponseDeliverTxSize(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -4609,6 +4835,28 @@ func TestResponseDeliverTxSize(t *testing.T) { } } +func TestResponseDeliverTxDeprecatedSize(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseDeliverTxDeprecated(popr, true) + size2 := github_com_gogo_protobuf_proto.Size(p) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + size := p.Size() + if len(dAtA) != size { + t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA)) + } + if size2 != size { + t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2) + } + size3 := github_com_gogo_protobuf_proto.Size(p) + if size3 != size { + t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3) + } +} + func TestResponseEndBlockSize(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) diff --git a/abci/types/util.go b/abci/types/util.go index 3cde88232..d71d4c8fc 100644 --- a/abci/types/util.go +++ b/abci/types/util.go @@ -32,3 +32,43 @@ func (v ValidatorUpdates) Swap(i, j int) { v[i] = v[j] v[j] = v1 } + +//------------------------------------------------------------------------------ + +func ConvertDeprecatedDeliverTxResponse(deprecated *ResponseDeliverTxDeprecated) *ResponseDeliverTx { + if deprecated == nil { + return nil + } + return &ResponseDeliverTx{ + Code: deprecated.Code, + Data: deprecated.Data, + Log: deprecated.Log, + Info: deprecated.Info, + GasWanted: deprecated.GasWanted, + GasUsed: deprecated.GasUsed, + Events: []Event{{Attributes: deprecated.Tags}}, + Codespace: deprecated.Codespace, + XXX_NoUnkeyedLiteral: deprecated.XXX_NoUnkeyedLiteral, + XXX_unrecognized: deprecated.XXX_unrecognized, + XXX_sizecache: deprecated.XXX_sizecache, + } +} + +func ConvertDeprecatedCheckTxResponse(deprecated *ResponseDeliverTxDeprecated) *ResponseCheckTx { + if deprecated == nil { + return nil + } + return &ResponseCheckTx{ + Code: deprecated.Code, + Data: deprecated.Data, + Log: deprecated.Log, + Info: deprecated.Info, + GasWanted: deprecated.GasWanted, + GasUsed: deprecated.GasUsed, + Events: []Event{{Attributes: deprecated.Tags}}, + Codespace: deprecated.Codespace, + XXX_NoUnkeyedLiteral: deprecated.XXX_NoUnkeyedLiteral, + XXX_unrecognized: deprecated.XXX_unrecognized, + XXX_sizecache: deprecated.XXX_sizecache, + } +} diff --git a/config/config.go b/config/config.go index 18ddbdb17..5592227f1 100644 --- a/config/config.go +++ b/config/config.go @@ -448,7 +448,7 @@ type RPCConfig struct { // DefaultRPCConfig returns a default configuration for the RPC server func DefaultRPCConfig() *RPCConfig { return &RPCConfig{ - ListenAddress: "tcp://127.0.0.1:26657", + ListenAddress: "tcp://0.0.0.0:26657", CORSAllowedOrigins: []string{}, CORSAllowedMethods: []string{"HEAD", "GET", "POST"}, CORSAllowedHeaders: []string{"Origin", "Accept", "Content-Type", "X-Requested-With", "X-Server-Time"}, diff --git a/rpc/core/types/responses.go b/rpc/core/types/responses.go index e53f9a53c..7619a899a 100644 --- a/rpc/core/types/responses.go +++ b/rpc/core/types/responses.go @@ -161,6 +161,13 @@ type ResultBroadcastTxCommit struct { Height int64 `json:"height"` } +type ResultBroadcastTxCommitDeprecated struct { + CheckTx abci.ResponseCheckTxDeprecated `json:"check_tx"` + DeliverTx abci.ResponseDeliverTxDeprecated `json:"deliver_tx"` + Hash cmn.HexBytes `json:"hash"` + Height int64 `json:"height"` +} + // Result of querying for a tx type ResultTx struct { Hash cmn.HexBytes `json:"hash"` @@ -171,6 +178,15 @@ type ResultTx struct { Proof types.TxProof `json:"proof,omitempty"` } +type ResultTxDeprecated struct { + Hash cmn.HexBytes `json:"hash"` + Height int64 `json:"height"` + Index uint32 `json:"index"` + TxResult abci.ResponseDeliverTxDeprecated `json:"tx_result"` + Tx types.Tx `json:"tx"` + Proof types.TxProof `json:"proof,omitempty"` +} + // Result of searching for txs type ResultTxSearch struct { Txs []*ResultTx `json:"txs"` diff --git a/state/store.go b/state/store.go index 4215cdd94..2c6dd73fe 100644 --- a/state/store.go +++ b/state/store.go @@ -145,6 +145,12 @@ type ABCIResponses struct { BeginBlock *abci.ResponseBeginBlock `json:"begin_block"` } +type ABCIResponsesDeprecated struct { + DeliverTx []*abci.ResponseDeliverTxDeprecated `json:"deliver_tx"` + EndBlock *abci.ResponseEndBlock `json:"end_block"` + BeginBlock *abci.ResponseBeginBlock `json:"begin_block"` +} + // NewABCIResponses returns a new ABCIResponses func NewABCIResponses(block *types.Block) *ABCIResponses { resDeliverTxs := make([]*abci.ResponseDeliverTx, block.NumTxs) @@ -179,9 +185,22 @@ func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) { abciResponses := new(ABCIResponses) err := cdc.UnmarshalBinaryBare(buf, abciResponses) if err != nil { - // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED - cmn.Exit(fmt.Sprintf(`LoadABCIResponses: Data has been corrupted or its spec has + abciResponsesDeprecated := new(ABCIResponsesDeprecated) + err := cdc.UnmarshalBinaryBare(buf, abciResponsesDeprecated) + if err != nil { + // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED + cmn.Exit(fmt.Sprintf(`LoadABCIResponses: Data has been corrupted or its spec has changed: %v\n`, err)) + } + var deliverTxs []*abci.ResponseDeliverTx + for _, result := range abciResponsesDeprecated.DeliverTx { + deliverTxs = append(deliverTxs, abci.ConvertDeprecatedDeliverTxResponse(result)) + } + return &ABCIResponses{ + DeliverTx: deliverTxs, + EndBlock: abciResponsesDeprecated.EndBlock, + BeginBlock: abciResponsesDeprecated.BeginBlock, + }, nil } // TODO: ensure that buf is completely read. diff --git a/state/txindex/kv/kv.go b/state/txindex/kv/kv.go index ec97d6012..33489d8d0 100644 --- a/state/txindex/kv/kv.go +++ b/state/txindex/kv/kv.go @@ -11,7 +11,7 @@ import ( "github.com/pkg/errors" - + abci "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/pubsub/query" @@ -78,7 +78,21 @@ func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) { txResult := new(types.TxResult) err := cdc.UnmarshalBinaryBare(rawBytes, &txResult) if err != nil { - return nil, fmt.Errorf("Error reading TxResult: %v", err) + txResultDeprecated := new(types.TxResultDeprecated) + err := cdc.UnmarshalBinaryBare(rawBytes, &txResultDeprecated) + if err != nil { + return nil, fmt.Errorf("Error reading TxResult: %v", err) + } + result := abci.ConvertDeprecatedDeliverTxResponse(&txResultDeprecated.Result) + if result == nil { + return nil, fmt.Errorf("Error: failed to convert deprecated tx result") + } + return &types.TxResult{ + Height: txResultDeprecated.Height, + Index: txResultDeprecated.Index, + Tx: txResultDeprecated.Tx, + Result: *result, + }, nil } return txResult, nil diff --git a/types/tx.go b/types/tx.go index 0c6845a7d..9cc787b34 100644 --- a/types/tx.go +++ b/types/tx.go @@ -122,6 +122,14 @@ type TxResult struct { Result abci.ResponseDeliverTx `json:"result"` } +// One usage is indexing transaction results. +type TxResultDeprecated struct { + Height int64 `json:"height"` + Index uint32 `json:"index"` + Tx Tx `json:"tx"` + Result abci.ResponseDeliverTxDeprecated `json:"result"` +} + // ComputeAminoOverhead calculates the overhead for amino encoding a transaction. // The overhead consists of varint encoding the field number and the wire type // (= length-delimited = 2), and another varint encoding the length of the From 95cf3001f9fc2aba3e7a02af4566618a1a02a5b6 Mon Sep 17 00:00:00 2001 From: fudongbai <296179868@qq.com> Date: Thu, 19 Sep 2019 18:24:58 +0800 Subject: [PATCH 194/211] export SaveABCIResponses --- state/execution.go | 2 +- state/export_test.go | 4 ++-- state/store.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/state/execution.go b/state/execution.go index 78dc0dbae..d6bb764e4 100644 --- a/state/execution.go +++ b/state/execution.go @@ -135,7 +135,7 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b fail.Fail() // XXX // Save the results before we commit. - saveABCIResponses(blockExec.db, block.Height, abciResponses) + SaveABCIResponses(blockExec.db, block.Height, abciResponses) fail.Fail() // XXX diff --git a/state/export_test.go b/state/export_test.go index af7f5cc23..0b7da945f 100644 --- a/state/export_test.go +++ b/state/export_test.go @@ -43,10 +43,10 @@ func CalcValidatorsKey(height int64) []byte { return calcValidatorsKey(height) } -// SaveABCIResponses is an alias for the private saveABCIResponses method in +// SaveABCIResponses is an alias for the private SaveABCIResponses method in // store.go, exported exclusively and explicitly for testing. func SaveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { - saveABCIResponses(db, height, abciResponses) + SaveABCIResponses(db, height, abciResponses) } // SaveConsensusParamsInfo is an alias for the private saveConsensusParamsInfo diff --git a/state/store.go b/state/store.go index 2c6dd73fe..2b23a05e3 100644 --- a/state/store.go +++ b/state/store.go @@ -210,7 +210,7 @@ func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) { // SaveABCIResponses persists the ABCIResponses to the database. // This is useful in case we crash after app.Commit and before s.Save(). // Responses are indexed by height so they can also be loaded later to produce Merkle proofs. -func saveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { +func SaveABCIResponses(db dbm.DB, height int64, abciResponses *ABCIResponses) { db.SetSync(calcABCIResponsesKey(height), abciResponses.Bytes()) } From c635865d51ce9bb9239cc4e5c2133f656bf0f4a8 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Thu, 19 Sep 2019 20:44:13 +0800 Subject: [PATCH 195/211] avoid to load state from stateDB for all block --- CHANGELOG.md | 1 - state/execution.go | 11 ++++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7644c0fff..6dfede18a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -136,7 +136,6 @@ program](https://hackerone.com/tendermint). option for `db_backend`. Must be `goleveldb` or `cleveldb`. - [rpc] [\#3616](https://github.com/tendermint/tendermint/issues/3616) Fix field names for `/block_results` response (eg. `results.DeliverTx` -> `results.deliver_tx`). See docs for details. - - [rpc] [\#3724](https://github.com/tendermint/tendermint/issues/3724) RPC now binds to `127.0.0.1` by default instead of `0.0.0.0` * Go API - [abci] [\#3193](https://github.com/tendermint/tendermint/issues/3193) Use RequestDeliverTx and RequestCheckTx in the ABCI diff --git a/state/execution.go b/state/execution.go index 78dc0dbae..a4c60d8b4 100644 --- a/state/execution.go +++ b/state/execution.go @@ -125,7 +125,7 @@ func (blockExec *BlockExecutor) ApplyBlock(state State, blockID types.BlockID, b } startTime := time.Now().UnixNano() - abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, blockExec.db) + abciResponses, err := execBlockOnProxyApp(blockExec.logger, blockExec.proxyApp, block, &state, blockExec.db) endTime := time.Now().UnixNano() blockExec.metrics.BlockProcessingTime.Observe(float64(endTime-startTime) / 1000000) if err != nil { @@ -244,6 +244,7 @@ func execBlockOnProxyApp( logger log.Logger, proxyAppConn proxy.AppConnConsensus, block *types.Block, + state *State, stateDB dbm.DB, ) (*ABCIResponses, error) { var validTxs, invalidTxs = 0, 0 @@ -270,7 +271,7 @@ func execBlockOnProxyApp( } proxyAppConn.SetResponseCallback(proxyCb) - commitInfo, byzVals := getBeginBlockValidatorInfo(block, stateDB) + commitInfo, byzVals := getBeginBlockValidatorInfo(block, state, stateDB) // Begin block var err error @@ -305,13 +306,12 @@ func execBlockOnProxyApp( return abciResponses, nil } -func getBeginBlockValidatorInfo(block *types.Block, stateDB dbm.DB) (abci.LastCommitInfo, []abci.Evidence) { +func getBeginBlockValidatorInfo(block *types.Block, state *State, stateDB dbm.DB) (abci.LastCommitInfo, []abci.Evidence) { voteInfos := make([]abci.VoteInfo, block.LastCommit.Size()) byzVals := make([]abci.Evidence, len(block.Evidence.Evidence)) var lastValSet *types.ValidatorSet var err error if block.Height > 1 { - state := LoadState(stateDB) // for state sync, validator set can't be load from db and it should be equal to the validator set in state if block.Height == state.LastBlockHeight + 1 { lastValSet = state.Validators @@ -493,7 +493,8 @@ func ExecCommitBlock( logger log.Logger, stateDB dbm.DB, ) ([]byte, error) { - _, err := execBlockOnProxyApp(logger, appConnConsensus, block, stateDB) + state := LoadState(stateDB) + _, err := execBlockOnProxyApp(logger, appConnConsensus, block, &state, stateDB) if err != nil { logger.Error("Error executing block on proxy app", "height", block.Height, "err", err) return nil, err From 0e5c824937869145e4978ddc99775f88ec4aff74 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Fri, 20 Sep 2019 11:32:20 +0800 Subject: [PATCH 196/211] roll back remove deprecated const --- CHANGELOG.md | 3 --- libs/db/c_level_db.go | 1 + libs/db/db.go | 5 +++++ libs/db/go_level_db.go | 1 + 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6dfede18a..19f326536 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -140,9 +140,6 @@ program](https://hackerone.com/tendermint). * Go API - [abci] [\#3193](https://github.com/tendermint/tendermint/issues/3193) Use RequestDeliverTx and RequestCheckTx in the ABCI Application interface - - [libs/db] [\#3632](https://github.com/tendermint/tendermint/pull/3632) Removed deprecated `LevelDBBackend` const - If you have `db_backend` set to `leveldb` in your config file, please - change it to `goleveldb` or `cleveldb`. - [p2p] [\#3521](https://github.com/tendermint/tendermint/issues/3521) Remove NewNetAddressStringWithOptionalID * Blockchain Protocol diff --git a/libs/db/c_level_db.go b/libs/db/c_level_db.go index 952b3f767..da95835ae 100644 --- a/libs/db/c_level_db.go +++ b/libs/db/c_level_db.go @@ -14,6 +14,7 @@ func init() { dbCreator := func(name string, dir string, opt interface{}) (DB, error) { return NewCLevelDB(name, dir) } + registerDBCreator(LevelDBBackend, dbCreator, true) registerDBCreator(CLevelDBBackend, dbCreator, false) } diff --git a/libs/db/db.go b/libs/db/db.go index c047827a9..44adc630e 100644 --- a/libs/db/db.go +++ b/libs/db/db.go @@ -9,6 +9,11 @@ type DBBackendType string // These are valid backend types. const ( + // LevelDBBackend is a legacy type. Defaults to goleveldb unless cleveldb + // build tag was used, in which it becomes cleveldb. + // Deprecated: Use concrete types (golevedb, cleveldb, etc.) + LevelDBBackend DBBackendType = "leveldb" + // GoLevelDBBackend represents goleveldb (github.com/syndtr/goleveldb - most // popular implementation) // - pure go diff --git a/libs/db/go_level_db.go b/libs/db/go_level_db.go index b484c1c74..2c1a716b3 100644 --- a/libs/db/go_level_db.go +++ b/libs/db/go_level_db.go @@ -19,6 +19,7 @@ func init() { return NewGoLevelDB(name, dir) } } + registerDBCreator(LevelDBBackend, dbCreator, false) registerDBCreator(GoLevelDBBackend, dbCreator, false) } From 12ae6ba1472d54bf4aaec10724de41c4c0ddd55d Mon Sep 17 00:00:00 2001 From: fudongbai <296179868@qq.com> Date: Fri, 20 Sep 2019 14:37:48 +0800 Subject: [PATCH 197/211] remove reduntant code --- abci/client/grpc_client.go | 3 ++- abci/types/result.go | 10 ---------- abci/types/util.go | 19 ------------------- blockchain/hot/reactor.go | 2 +- node/node.go | 1 + 5 files changed, 4 insertions(+), 31 deletions(-) diff --git a/abci/client/grpc_client.go b/abci/client/grpc_client.go index 38ae64a36..337ad7778 100644 --- a/abci/client/grpc_client.go +++ b/abci/client/grpc_client.go @@ -65,6 +65,7 @@ RETRY_LOOP: ENSURE_CONNECTED: for { + // FailFast is deprecated _, err := client.Echo(context.Background(), &types.RequestEcho{Message: "hello"}, grpc.WaitForReady(true)) if err == nil { break ENSURE_CONNECTED @@ -179,7 +180,7 @@ func (cli *grpcClient) CheckTxAsync(params types.RequestCheckTx) *ReqRes { func (cli *grpcClient) ReCheckTxAsync(tx types.RequestCheckTx) *ReqRes { req := types.ToRequestCheckTx(tx) - res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.FailFast(true)) + res, err := cli.client.CheckTx(context.Background(), req.GetCheckTx(), grpc.WaitForReady(true)) if err != nil { cli.StopForError(err) } diff --git a/abci/types/result.go b/abci/types/result.go index d0deb42d7..dbf409f4c 100644 --- a/abci/types/result.go +++ b/abci/types/result.go @@ -31,16 +31,6 @@ func (r ResponseDeliverTx) IsErr() bool { return r.Code != CodeTypeOK } -// IsOK returns true if Code is OK. -func (r ResponseDeliverTxDeprecated) IsOK() bool { - return r.Code == CodeTypeOK -} - -// IsErr returns true if Code is something other than OK. -func (r ResponseDeliverTxDeprecated) IsErr() bool { - return r.Code != CodeTypeOK -} - // IsOK returns true if Code is OK. func (r ResponseQuery) IsOK() bool { return r.Code == CodeTypeOK diff --git a/abci/types/util.go b/abci/types/util.go index d71d4c8fc..2afcd2d75 100644 --- a/abci/types/util.go +++ b/abci/types/util.go @@ -53,22 +53,3 @@ func ConvertDeprecatedDeliverTxResponse(deprecated *ResponseDeliverTxDeprecated) XXX_sizecache: deprecated.XXX_sizecache, } } - -func ConvertDeprecatedCheckTxResponse(deprecated *ResponseDeliverTxDeprecated) *ResponseCheckTx { - if deprecated == nil { - return nil - } - return &ResponseCheckTx{ - Code: deprecated.Code, - Data: deprecated.Data, - Log: deprecated.Log, - Info: deprecated.Info, - GasWanted: deprecated.GasWanted, - GasUsed: deprecated.GasUsed, - Events: []Event{{Attributes: deprecated.Tags}}, - Codespace: deprecated.Codespace, - XXX_NoUnkeyedLiteral: deprecated.XXX_NoUnkeyedLiteral, - XXX_unrecognized: deprecated.XXX_unrecognized, - XXX_sizecache: deprecated.XXX_sizecache, - } -} diff --git a/blockchain/hot/reactor.go b/blockchain/hot/reactor.go index 2013a7c2e..c9802837e 100644 --- a/blockchain/hot/reactor.go +++ b/blockchain/hot/reactor.go @@ -3,13 +3,13 @@ package hot import ( "errors" "fmt" - "github.com/tendermint/tendermint/store" "reflect" "time" "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/p2p" + "github.com/tendermint/tendermint/store" sm "github.com/tendermint/tendermint/state" "github.com/tendermint/tendermint/types" ) diff --git a/node/node.go b/node/node.go index 698fba903..4f3fb68fa 100644 --- a/node/node.go +++ b/node/node.go @@ -519,6 +519,7 @@ func createSwitch(config *cfg.Config, sw.AddReactor("BLOCKCHAIN", bcReactor) sw.AddReactor("CONSENSUS", consensusReactor) sw.AddReactor("EVIDENCE", evidenceReactor) + // stateReactor is special, need create it before block chain reactor. if stateReactor != nil { sw.AddReactor("STATE", stateReactor) } From 337de3e4ef654e1a01d3b5f7dbdfbe3be3eab70c Mon Sep 17 00:00:00 2001 From: fudongbai <296179868@qq.com> Date: Fri, 20 Sep 2019 15:49:56 +0800 Subject: [PATCH 198/211] fix tendermint will crash when an evil peer send nil pubkey during handshake --- p2p/conn/secret_connection.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/p2p/conn/secret_connection.go b/p2p/conn/secret_connection.go index a4489f475..6c42e4957 100644 --- a/p2p/conn/secret_connection.go +++ b/p2p/conn/secret_connection.go @@ -133,6 +133,9 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (* } remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig + if remPubKey == nil{ + return nil, errors.New("pubkey is missing") + } if !remPubKey.VerifyBytes(challenge[:], remSignature) { return nil, errors.New("Challenge verification failed") } From 42986fd25f62bcd767612dc1cb0b725f0f053e78 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Fri, 20 Sep 2019 16:21:23 +0800 Subject: [PATCH 199/211] Fix wrong dbcreator for boltDB --- libs/db/boltdb.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/db/boltdb.go b/libs/db/boltdb.go index 30501dd82..0e38a417e 100644 --- a/libs/db/boltdb.go +++ b/libs/db/boltdb.go @@ -15,7 +15,7 @@ import ( var bucket = []byte("tm") func init() { - registerDBCreator(BoltDBBackend, func(name, dir string) (DB, error) { + registerDBCreator(BoltDBBackend, func(name, dir string, opt interface{}) (DB, error) { return NewBoltDB(name, dir) }, false) } From cd6734254fca93c5e5e69fefeaa9e13caa94075e Mon Sep 17 00:00:00 2001 From: fudongbai <296179868@qq.com> Date: Mon, 23 Sep 2019 17:24:54 +0800 Subject: [PATCH 200/211] add nil check to message --- consensus/reactor.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/consensus/reactor.go b/consensus/reactor.go index a4e55a7f5..d088bc793 100644 --- a/consensus/reactor.go +++ b/consensus/reactor.go @@ -1538,6 +1538,9 @@ func (m *BlockPartMessage) ValidateBasic() error { if m.Round < 0 { return errors.New("Negative Round") } + if m.Part == nil { + return errors.New("block part is missing") + } if err := m.Part.ValidateBasic(); err != nil { return fmt.Errorf("Wrong Part: %v", err) } @@ -1558,6 +1561,9 @@ type VoteMessage struct { // ValidateBasic performs basic validation. func (m *VoteMessage) ValidateBasic() error { + if m.Vote == nil { + return errors.New("vote is missing") + } return m.Vote.ValidateBasic() } From 479a59a5dbd72c50dded71c6c8dfb9504ebf7592 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Mon, 23 Sep 2019 19:49:17 +0800 Subject: [PATCH 201/211] cleveldb is enabled only when dbbackend in config is cleveldb --- libs/db/c_level_db.go | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/db/c_level_db.go b/libs/db/c_level_db.go index da95835ae..952b3f767 100644 --- a/libs/db/c_level_db.go +++ b/libs/db/c_level_db.go @@ -14,7 +14,6 @@ func init() { dbCreator := func(name string, dir string, opt interface{}) (DB, error) { return NewCLevelDB(name, dir) } - registerDBCreator(LevelDBBackend, dbCreator, true) registerDBCreator(CLevelDBBackend, dbCreator, false) } From ccdea539a4050f7f2eaf639719b3f87b19c8dde9 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Tue, 8 Oct 2019 11:14:02 +0800 Subject: [PATCH 202/211] Move msg decoder forward, make msg decoding sync --- mempool/reactor.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/mempool/reactor.go b/mempool/reactor.go index adbc4fab9..e9d490454 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -33,7 +33,7 @@ const ( type MempoolPacket struct { chID byte src p2p.Peer - msgBytes []byte + msg MempoolMessage } // Reactor handles mempool tx broadcasting amongst peers. @@ -169,26 +169,25 @@ func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { // Receive implements Reactor. func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - memR.recvCh <- &MempoolPacket{chID: chID, src: src, msgBytes: msgBytes} + msg, err := memR.decodeMsg(msgBytes) + if err != nil { + memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + memR.Switch.StopPeerForError(src, err) + return + } + memR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) + memR.recvCh <- &MempoolPacket{chID: chID, src: src, msg: msg} } func (memR *Reactor) receiveRoutine() { memR.Logger.Debug("Starting ReceiveRoutine for mempool") for p := range memR.recvCh { - memR.receiveImpl(p.chID, p.src, p.msgBytes) + memR.receiveImpl(p.chID, p.src, p.msg) } } // It adds any received transactions to the mempool. -func (memR *Reactor) receiveImpl(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := memR.decodeMsg(msgBytes) - if err != nil { - memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) - memR.Switch.StopPeerForError(src, err) - return - } - memR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) - +func (memR *Reactor) receiveImpl(chID byte, src p2p.Peer, msg MempoolMessage) { switch msg := msg.(type) { case *TxMessage: peerID := memR.ids.GetForPeer(src) From 3aa28d24890452bcbaa6253bd896292e007ab674 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 8 Oct 2019 12:52:28 -0500 Subject: [PATCH 203/211] p2p: only allow ed25519 pubkeys when connecting, also recover from any possible failures in acceptPeers --- crypto/multisig/threshold_pubkey.go | 8 +++++ p2p/conn/secret_connection.go | 19 ++++++------ p2p/conn/secret_connection_test.go | 47 +++++++++++++++++++++++++++++ p2p/transport.go | 19 ++++++++++++ 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/crypto/multisig/threshold_pubkey.go b/crypto/multisig/threshold_pubkey.go index 234d420f1..5ed7651cb 100644 --- a/crypto/multisig/threshold_pubkey.go +++ b/crypto/multisig/threshold_pubkey.go @@ -21,6 +21,11 @@ func NewPubKeyMultisigThreshold(k int, pubkeys []crypto.PubKey) crypto.PubKey { if len(pubkeys) < k { panic("threshold k of n multisignature: len(pubkeys) < k") } + for _, pubkey := range pubkeys { + if pubkey == nil { + panic("nil pubkey") + } + } return PubKeyMultisigThreshold{uint(k), pubkeys} } @@ -53,6 +58,9 @@ func (pk PubKeyMultisigThreshold) VerifyBytes(msg []byte, marshalledSig []byte) sigIndex := 0 for i := 0; i < size; i++ { if sig.BitArray.GetIndex(i) { + if pk.PubKeys[i] == nil { + return false + } if !pk.PubKeys[i].VerifyBytes(msg, sig.Sigs[sigIndex]) { return false } diff --git a/p2p/conn/secret_connection.go b/p2p/conn/secret_connection.go index 6c42e4957..2481c04ca 100644 --- a/p2p/conn/secret_connection.go +++ b/p2p/conn/secret_connection.go @@ -7,21 +7,22 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/binary" - "errors" "io" "math" "net" "sync" "time" + pool "github.com/libp2p/go-buffer-pool" + "github.com/pkg/errors" "golang.org/x/crypto/chacha20poly1305" "golang.org/x/crypto/curve25519" + "golang.org/x/crypto/hkdf" "golang.org/x/crypto/nacl/box" - pool "github.com/libp2p/go-buffer-pool" "github.com/tendermint/tendermint/crypto" + "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" - "golang.org/x/crypto/hkdf" ) // 4 + 1024 == 1028 total frame size @@ -107,11 +108,11 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (* sendAead, err := chacha20poly1305.New(sendSecret[:]) if err != nil { - return nil, errors.New("Invalid send SecretConnection Key") + return nil, errors.New("invalid send SecretConnection Key") } recvAead, err := chacha20poly1305.New(recvSecret[:]) if err != nil { - return nil, errors.New("Invalid receive SecretConnection Key") + return nil, errors.New("invalid receive SecretConnection Key") } // Construct SecretConnection. sc := &SecretConnection{ @@ -133,11 +134,11 @@ func MakeSecretConnection(conn io.ReadWriteCloser, locPrivKey crypto.PrivKey) (* } remPubKey, remSignature := authSigMsg.Key, authSigMsg.Sig - if remPubKey == nil{ - return nil, errors.New("pubkey is missing") + if _, ok := remPubKey.(ed25519.PubKeyEd25519); !ok { + return nil, errors.Errorf("expected ed25519 pubkey, got %T", remPubKey) } if !remPubKey.VerifyBytes(challenge[:], remSignature) { - return nil, errors.New("Challenge verification failed") + return nil, errors.New("challenge verification failed") } // We've authorized. @@ -220,7 +221,7 @@ func (sc *SecretConnection) Read(data []byte) (n int, err error) { defer pool.Put(frame) _, err = sc.recvAead.Open(frame[:0], sc.recvNonce[:], sealedFrame, nil) if err != nil { - return n, errors.New("Failed to decrypt SecretConnection") + return n, errors.New("failed to decrypt SecretConnection") } incrNonce(sc.recvNonce) // end decryption diff --git a/p2p/conn/secret_connection_test.go b/p2p/conn/secret_connection_test.go index 9ab9695a3..daa1e98a7 100644 --- a/p2p/conn/secret_connection_test.go +++ b/p2p/conn/secret_connection_test.go @@ -17,7 +17,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" + "github.com/tendermint/tendermint/crypto/secp256k1" cmn "github.com/tendermint/tendermint/libs/common" ) @@ -364,6 +366,51 @@ func TestDeriveSecretsAndChallengeGolden(t *testing.T) { } } +type privKeyWithNilPubKey struct { + orig crypto.PrivKey +} + +func (pk privKeyWithNilPubKey) Bytes() []byte { return pk.orig.Bytes() } +func (pk privKeyWithNilPubKey) Sign(msg []byte) ([]byte, error) { return pk.orig.Sign(msg) } +func (pk privKeyWithNilPubKey) PubKey() crypto.PubKey { return nil } +func (pk privKeyWithNilPubKey) Equals(pk2 crypto.PrivKey) bool { return pk.orig.Equals(pk2) } + +func TestNilPubkey(t *testing.T) { + var fooConn, barConn = makeKVStoreConnPair() + var fooPrvKey = ed25519.GenPrivKey() + var barPrvKey = privKeyWithNilPubKey{ed25519.GenPrivKey()} + + go func() { + _, err := MakeSecretConnection(barConn, barPrvKey) + assert.NoError(t, err) + }() + + assert.NotPanics(t, func() { + _, err := MakeSecretConnection(fooConn, fooPrvKey) + if assert.Error(t, err) { + assert.Equal(t, "expected ed25519 pubkey, got ", err.Error()) + } + }) +} + +func TestNonEd25519Pubkey(t *testing.T) { + var fooConn, barConn = makeKVStoreConnPair() + var fooPrvKey = ed25519.GenPrivKey() + var barPrvKey = secp256k1.GenPrivKey() + + go func() { + _, err := MakeSecretConnection(barConn, barPrvKey) + assert.NoError(t, err) + }() + + assert.NotPanics(t, func() { + _, err := MakeSecretConnection(fooConn, fooPrvKey) + if assert.Error(t, err) { + assert.Equal(t, "expected ed25519 pubkey, got secp256k1.PubKeySecp256k1", err.Error()) + } + }) +} + // Creates the data for a test vector file. // The file format is: // Hex(diffie_hellman_secret), loc_is_least, Hex(recvSecret), Hex(sendSecret), Hex(challenge) diff --git a/p2p/transport.go b/p2p/transport.go index 93bb35510..8fec2ba14 100644 --- a/p2p/transport.go +++ b/p2p/transport.go @@ -6,6 +6,8 @@ import ( "net" "time" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/p2p/conn" ) @@ -270,6 +272,23 @@ func (mt *MultiplexTransport) acceptPeers() { // // [0] https://en.wikipedia.org/wiki/Head-of-line_blocking go func(c net.Conn) { + defer func() { + if r := recover(); r != nil { + err := ErrRejected{ + conn: c, + err: errors.Errorf("recovered from panic: %v", r), + isAuthFailure: true, + } + select { + case mt.acceptc <- accept{err: err}: + case <-mt.closec: + // Give up if the transport was closed. + _ = c.Close() + return + } + } + }() + var ( nodeInfo NodeInfo secretConn *conn.SecretConnection From 816896b267fac1ca51eea4569bfd6a20665a3b7e Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Tue, 15 Oct 2019 20:09:40 +0800 Subject: [PATCH 204/211] Revert "R4R: make msg decoding sync" --- mempool/reactor.go | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/mempool/reactor.go b/mempool/reactor.go index e9d490454..adbc4fab9 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -33,7 +33,7 @@ const ( type MempoolPacket struct { chID byte src p2p.Peer - msg MempoolMessage + msgBytes []byte } // Reactor handles mempool tx broadcasting amongst peers. @@ -169,25 +169,26 @@ func (memR *Reactor) RemovePeer(peer p2p.Peer, reason interface{}) { // Receive implements Reactor. func (memR *Reactor) Receive(chID byte, src p2p.Peer, msgBytes []byte) { - msg, err := memR.decodeMsg(msgBytes) - if err != nil { - memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) - memR.Switch.StopPeerForError(src, err) - return - } - memR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) - memR.recvCh <- &MempoolPacket{chID: chID, src: src, msg: msg} + memR.recvCh <- &MempoolPacket{chID: chID, src: src, msgBytes: msgBytes} } func (memR *Reactor) receiveRoutine() { memR.Logger.Debug("Starting ReceiveRoutine for mempool") for p := range memR.recvCh { - memR.receiveImpl(p.chID, p.src, p.msg) + memR.receiveImpl(p.chID, p.src, p.msgBytes) } } // It adds any received transactions to the mempool. -func (memR *Reactor) receiveImpl(chID byte, src p2p.Peer, msg MempoolMessage) { +func (memR *Reactor) receiveImpl(chID byte, src p2p.Peer, msgBytes []byte) { + msg, err := memR.decodeMsg(msgBytes) + if err != nil { + memR.Logger.Error("Error decoding message", "src", src, "chId", chID, "msg", msg, "err", err, "bytes", msgBytes) + memR.Switch.StopPeerForError(src, err) + return + } + memR.Logger.Debug("Receive", "src", src, "chId", chID, "msg", msg) + switch msg := msg.(type) { case *TxMessage: peerID := memR.ids.GetForPeer(src) From cfd235ccaf7709da28de0b17ef14c57a5adb9281 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Thu, 24 Oct 2019 17:44:51 +0800 Subject: [PATCH 205/211] call recheck abci interface in mempool --- abci/client/local_client.go | 2 +- mempool/clist_mempool.go | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/abci/client/local_client.go b/abci/client/local_client.go index 8eabe630b..476d3c680 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -106,7 +106,7 @@ func (app *localClient) CheckTxAsync(req types.RequestCheckTx) *ReqRes { func (app *localClient) ReCheckTxAsync(req types.RequestCheckTx) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.CheckTx(req) + res := app.Application.ReCheckTx(req) return app.callback( types.ToRequestCheckTx(req), types.ToResponseCheckTx(res), diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index ed5fee485..81b8f96ba 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -587,9 +587,8 @@ func (mem *CListMempool) recheckTxs() { // NOTE: globalCb may be called concurrently. for e := mem.txs.Front(); e != nil; e = e.Next() { memTx := e.Value.(*mempoolTx) - mem.proxyAppConn.CheckTxAsync(abci.RequestCheckTx{ + mem.proxyAppConn.ReCheckTxAsync(abci.RequestCheckTx{ Tx: memTx.tx, - Type: abci.CheckTxType_Recheck, }) } From a4cd496e135605bcf9bb04c6c945c8df3766ef5c Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Wed, 30 Oct 2019 19:02:36 +0800 Subject: [PATCH 206/211] restore the mtx refactor in mempool --- mempool/clist_mempool.go | 67 ++++++++++++++++++++++++---------------- mempool/reactor.go | 2 +- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 81b8f96ba..4050a3d16 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -31,11 +31,13 @@ import ( type CListMempool struct { config *cfg.MempoolConfig - proxyMtx sync.Mutex - proxyAppConn proxy.AppConnMempool - txs *clist.CList // concurrent linked-list of good txs - preCheck PreCheckFunc - postCheck PostCheckFunc + proxyLowMtx sync.Mutex + proxyNextMtx sync.Mutex + proxyBlockingMtx sync.Mutex + proxyAppConn proxy.AppConnMempool + txs *clist.CList // concurrent linked-list of good txs + preCheck PreCheckFunc + postCheck PostCheckFunc // Track whether we're rechecking txs. // These are not protected by a mutex and are expected to be mutated @@ -136,18 +138,18 @@ func (mem *CListMempool) InitWAL() { walDir := mem.config.WalDir() err := cmn.EnsureDir(walDir, 0700) if err != nil { - panic(errors.Wrap(err, "Error ensuring WAL dir")) + panic(errors.Wrap(err, "Error ensuring Mempool WAL dir")) } af, err := auto.OpenAutoFile(walDir + "/wal") if err != nil { - panic(errors.Wrap(err, "Error opening WAL file")) + panic(errors.Wrap(err, "Error opening Mempool WAL file")) } mem.wal = af } func (mem *CListMempool) CloseWAL() { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() + mem.Lock() + defer mem.Unlock() if err := mem.wal.Close(); err != nil { mem.logger.Error("Error closing WAL", "err", err) @@ -156,11 +158,25 @@ func (mem *CListMempool) CloseWAL() { } func (mem *CListMempool) Lock() { - mem.proxyMtx.Lock() + mem.proxyNextMtx.Lock() + mem.proxyBlockingMtx.Lock() + mem.proxyNextMtx.Unlock() } func (mem *CListMempool) Unlock() { - mem.proxyMtx.Unlock() + mem.proxyBlockingMtx.Unlock() +} + +func (mem *CListMempool) LockLow() { + mem.proxyLowMtx.Lock() + mem.proxyNextMtx.Lock() + mem.proxyBlockingMtx.Lock() + mem.proxyNextMtx.Unlock() +} + +func (mem *CListMempool) UnlockLow() { + mem.proxyBlockingMtx.Unlock() + mem.proxyLowMtx.Unlock() } func (mem *CListMempool) Size() int { @@ -176,8 +192,8 @@ func (mem *CListMempool) FlushAppConn() error { } func (mem *CListMempool) Flush() { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() + mem.Lock() + defer mem.Unlock() mem.cache.Reset() @@ -192,7 +208,6 @@ func (mem *CListMempool) Flush() { // TxsFront returns the first transaction in the ordered list for peer // goroutines to call .NextWait() on. -// FIXME: leaking implementation details! func (mem *CListMempool) TxsFront() *clist.CElement { return mem.txs.Front() } @@ -213,9 +228,9 @@ func (mem *CListMempool) CheckTx(tx types.Tx, cb func(*abci.Response)) (err erro } func (mem *CListMempool) CheckTxWithInfo(tx types.Tx, cb func(*abci.Response), txInfo TxInfo) (err error) { - mem.proxyMtx.Lock() + mem.LockLow() // use defer to unlock mutex because application (*local client*) might panic - defer mem.proxyMtx.Unlock() + defer mem.UnlockLow() var ( memSize = mem.Size() @@ -379,7 +394,7 @@ func (mem *CListMempool) resCbFirstTime(tx []byte, txInfo TxInfo, res *abci.Resp mem.notifyTxsAvailable() } else { // ignore bad transaction - mem.logger.Info("Rejected bad transaction", "tx", txID(tx), "res", r, "err", postCheckErr) + mem.logger.Info("Rejected bad transaction", "tx", TxID(tx), "res", r, "err", postCheckErr) mem.metrics.FailedTxs.Add(1) // remove from cache (it might be good later) mem.cache.Remove(tx) @@ -412,7 +427,7 @@ func (mem *CListMempool) resCbRecheck(req *abci.Request, res *abci.Response) { // Good, nothing to do. } else { // Tx became invalidated due to newly committed block. - mem.logger.Info("Tx is no longer valid", "tx", txID(tx), "res", r, "err", postCheckErr) + mem.logger.Info("Tx is no longer valid", "tx", TxID(tx), "res", r, "err", postCheckErr) // NOTE: we remove tx from the cache because it might be good later mem.removeTx(tx, mem.recheckCursor, true) } @@ -455,8 +470,8 @@ func (mem *CListMempool) notifyTxsAvailable() { } func (mem *CListMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() + mem.Lock() + defer mem.Unlock() for atomic.LoadInt32(&mem.rechecking) > 0 { // TODO: Something better? @@ -492,8 +507,8 @@ func (mem *CListMempool) ReapMaxBytesMaxGas(maxBytes, maxGas int64) types.Txs { } func (mem *CListMempool) ReapMaxTxs(max int) types.Txs { - mem.proxyMtx.Lock() - defer mem.proxyMtx.Unlock() + mem.Lock() + defer mem.Unlock() if max < 0 { max = mem.txs.Len() @@ -556,9 +571,9 @@ func (mem *CListMempool) Update( // Either recheck non-committed txs to see if they became invalid // or just notify there're some txs left. - if mem.Size() > 0 { + if memSize := mem.Size(); memSize > 0 { if mem.config.Recheck { - mem.logger.Info("Recheck txs", "numtxs", mem.Size(), "height", height) + mem.logger.Info("Recheck txs", "numtxs", memSize, "height", height) mem.recheckTxs() // At this point, mem.txs are being rechecked. // mem.recheckCursor re-scans mem.txs and possibly removes some txs. @@ -704,7 +719,7 @@ func txKey(tx types.Tx) [sha256.Size]byte { return sha256.Sum256(tx) } -// txID is the hex encoded hash of the bytes as a types.Tx. -func txID(tx []byte) string { +// TxID is the hex encoded hash of the bytes as a types.Tx. +func TxID(tx []byte) string { return fmt.Sprintf("%X", types.Tx(tx).Hash()) } diff --git a/mempool/reactor.go b/mempool/reactor.go index f2bb5c92f..6097d4a39 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -198,7 +198,7 @@ func (memR *Reactor) receiveImpl(chID byte, src p2p.Peer, msgBytes []byte) { if err == ErrTxInCache { memR.Logger.Debug("Could not check tx", "tx", "err", err) } else { - memR.Logger.Info("Could not check tx", "tx", txID(msg.Tx), "err", err) + memR.Logger.Info("Could not check tx", "tx", TxID(msg.Tx), "err", err) } } // broadcasting happens from go routines per peer From f4a15fcfe50269c2dd2f8eed3c70cbeddcc56628 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Wed, 30 Oct 2019 19:21:46 +0800 Subject: [PATCH 207/211] Add checkTx type --- mempool/clist_mempool.go | 1 + 1 file changed, 1 insertion(+) diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 4050a3d16..044f9808c 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -604,6 +604,7 @@ func (mem *CListMempool) recheckTxs() { memTx := e.Value.(*mempoolTx) mem.proxyAppConn.ReCheckTxAsync(abci.RequestCheckTx{ Tx: memTx.tx, + Type: abci.CheckTxType_Recheck, }) } From 920983156650600116a55d5193690c0e581c5dec Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Thu, 31 Oct 2019 10:15:19 +0800 Subject: [PATCH 208/211] use lowercase for txID utils --- mempool/clist_mempool.go | 8 ++++---- mempool/reactor.go | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mempool/clist_mempool.go b/mempool/clist_mempool.go index 044f9808c..7cd7fd49a 100644 --- a/mempool/clist_mempool.go +++ b/mempool/clist_mempool.go @@ -394,7 +394,7 @@ func (mem *CListMempool) resCbFirstTime(tx []byte, txInfo TxInfo, res *abci.Resp mem.notifyTxsAvailable() } else { // ignore bad transaction - mem.logger.Info("Rejected bad transaction", "tx", TxID(tx), "res", r, "err", postCheckErr) + mem.logger.Info("Rejected bad transaction", "tx", txID(tx), "res", r, "err", postCheckErr) mem.metrics.FailedTxs.Add(1) // remove from cache (it might be good later) mem.cache.Remove(tx) @@ -427,7 +427,7 @@ func (mem *CListMempool) resCbRecheck(req *abci.Request, res *abci.Response) { // Good, nothing to do. } else { // Tx became invalidated due to newly committed block. - mem.logger.Info("Tx is no longer valid", "tx", TxID(tx), "res", r, "err", postCheckErr) + mem.logger.Info("Tx is no longer valid", "tx", txID(tx), "res", r, "err", postCheckErr) // NOTE: we remove tx from the cache because it might be good later mem.removeTx(tx, mem.recheckCursor, true) } @@ -720,7 +720,7 @@ func txKey(tx types.Tx) [sha256.Size]byte { return sha256.Sum256(tx) } -// TxID is the hex encoded hash of the bytes as a types.Tx. -func TxID(tx []byte) string { +// txID is the hex encoded hash of the bytes as a types.Tx. +func txID(tx []byte) string { return fmt.Sprintf("%X", types.Tx(tx).Hash()) } diff --git a/mempool/reactor.go b/mempool/reactor.go index 6097d4a39..c117353c0 100644 --- a/mempool/reactor.go +++ b/mempool/reactor.go @@ -194,11 +194,11 @@ func (memR *Reactor) receiveImpl(chID byte, src p2p.Peer, msgBytes []byte) { peerID := memR.ids.GetForPeer(src) err := memR.mempool.CheckTxWithInfo(msg.Tx, nil, TxInfo{SenderID: peerID, FromPersistent: src.IsPersistent()}) if err != nil { - // TxID(msg.Tx) is costly + // txID(msg.Tx) is costly if err == ErrTxInCache { memR.Logger.Debug("Could not check tx", "tx", "err", err) } else { - memR.Logger.Info("Could not check tx", "tx", TxID(msg.Tx), "err", err) + memR.Logger.Info("Could not check tx", "tx", txID(msg.Tx), "err", err) } } // broadcasting happens from go routines per peer From d78bdf36f26f476aa58b2d15a64d925f2a5bbdc3 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Thu, 31 Oct 2019 10:17:14 +0800 Subject: [PATCH 209/211] rename variable name --- abci/client/local_client.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/abci/client/local_client.go b/abci/client/local_client.go index 476d3c680..30cf57ae0 100644 --- a/abci/client/local_client.go +++ b/abci/client/local_client.go @@ -81,13 +81,13 @@ func (app *localClient) SetOptionAsync(req types.RequestSetOption) *ReqRes { ) } -func (app *localClient) DeliverTxAsync(params types.RequestDeliverTx) *ReqRes { +func (app *localClient) DeliverTxAsync(req types.RequestDeliverTx) *ReqRes { app.mtx.Lock() defer app.mtx.Unlock() - res := app.Application.DeliverTx(params) + res := app.Application.DeliverTx(req) return app.callback( - types.ToRequestDeliverTx(params), + types.ToRequestDeliverTx(req), types.ToResponseDeliverTx(res), ) } From 5f8a0e2f56c8efdecb7f861ac28fe4751f3c8c5d Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Mon, 4 Nov 2019 23:21:20 +0800 Subject: [PATCH 210/211] Add ResponseEndBlockDeprecated and ResponseBeginBlockDeprecated --- abci/types/types.pb.go | 1271 ++++++++++++++++++++++++++---------- abci/types/types.proto | 10 + abci/types/typespb_test.go | 248 +++++++ 3 files changed, 1192 insertions(+), 337 deletions(-) diff --git a/abci/types/types.pb.go b/abci/types/types.pb.go index ad525468d..490253d2f 100644 --- a/abci/types/types.pb.go +++ b/abci/types/types.pb.go @@ -58,7 +58,7 @@ func (x CheckTxType) String() string { return proto.EnumName(CheckTxType_name, int32(x)) } func (CheckTxType) EnumDescriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{0} + return fileDescriptor_types_765aa6431c012acb, []int{0} } type Request struct { @@ -84,7 +84,7 @@ func (m *Request) Reset() { *m = Request{} } func (m *Request) String() string { return proto.CompactTextString(m) } func (*Request) ProtoMessage() {} func (*Request) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{0} + return fileDescriptor_types_765aa6431c012acb, []int{0} } func (m *Request) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -506,7 +506,7 @@ func (m *RequestEcho) Reset() { *m = RequestEcho{} } func (m *RequestEcho) String() string { return proto.CompactTextString(m) } func (*RequestEcho) ProtoMessage() {} func (*RequestEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{1} + return fileDescriptor_types_765aa6431c012acb, []int{1} } func (m *RequestEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -552,7 +552,7 @@ func (m *RequestFlush) Reset() { *m = RequestFlush{} } func (m *RequestFlush) String() string { return proto.CompactTextString(m) } func (*RequestFlush) ProtoMessage() {} func (*RequestFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{2} + return fileDescriptor_types_765aa6431c012acb, []int{2} } func (m *RequestFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -594,7 +594,7 @@ func (m *RequestInfo) Reset() { *m = RequestInfo{} } func (m *RequestInfo) String() string { return proto.CompactTextString(m) } func (*RequestInfo) ProtoMessage() {} func (*RequestInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{3} + return fileDescriptor_types_765aa6431c012acb, []int{3} } func (m *RequestInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -657,7 +657,7 @@ func (m *RequestSetOption) Reset() { *m = RequestSetOption{} } func (m *RequestSetOption) String() string { return proto.CompactTextString(m) } func (*RequestSetOption) ProtoMessage() {} func (*RequestSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{4} + return fileDescriptor_types_765aa6431c012acb, []int{4} } func (m *RequestSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -715,7 +715,7 @@ func (m *RequestInitChain) Reset() { *m = RequestInitChain{} } func (m *RequestInitChain) String() string { return proto.CompactTextString(m) } func (*RequestInitChain) ProtoMessage() {} func (*RequestInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{5} + return fileDescriptor_types_765aa6431c012acb, []int{5} } func (m *RequestInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -793,7 +793,7 @@ func (m *RequestQuery) Reset() { *m = RequestQuery{} } func (m *RequestQuery) String() string { return proto.CompactTextString(m) } func (*RequestQuery) ProtoMessage() {} func (*RequestQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{6} + return fileDescriptor_types_765aa6431c012acb, []int{6} } func (m *RequestQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -864,7 +864,7 @@ func (m *RequestBeginBlock) Reset() { *m = RequestBeginBlock{} } func (m *RequestBeginBlock) String() string { return proto.CompactTextString(m) } func (*RequestBeginBlock) ProtoMessage() {} func (*RequestBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{7} + return fileDescriptor_types_765aa6431c012acb, []int{7} } func (m *RequestBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -933,7 +933,7 @@ func (m *RequestCheckTx) Reset() { *m = RequestCheckTx{} } func (m *RequestCheckTx) String() string { return proto.CompactTextString(m) } func (*RequestCheckTx) ProtoMessage() {} func (*RequestCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{8} + return fileDescriptor_types_765aa6431c012acb, []int{8} } func (m *RequestCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -987,7 +987,7 @@ func (m *RequestDeliverTx) Reset() { *m = RequestDeliverTx{} } func (m *RequestDeliverTx) String() string { return proto.CompactTextString(m) } func (*RequestDeliverTx) ProtoMessage() {} func (*RequestDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{9} + return fileDescriptor_types_765aa6431c012acb, []int{9} } func (m *RequestDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1034,7 +1034,7 @@ func (m *RequestEndBlock) Reset() { *m = RequestEndBlock{} } func (m *RequestEndBlock) String() string { return proto.CompactTextString(m) } func (*RequestEndBlock) ProtoMessage() {} func (*RequestEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{10} + return fileDescriptor_types_765aa6431c012acb, []int{10} } func (m *RequestEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1080,7 +1080,7 @@ func (m *RequestCommit) Reset() { *m = RequestCommit{} } func (m *RequestCommit) String() string { return proto.CompactTextString(m) } func (*RequestCommit) ProtoMessage() {} func (*RequestCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{11} + return fileDescriptor_types_765aa6431c012acb, []int{11} } func (m *RequestCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1133,7 +1133,7 @@ func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{12} + return fileDescriptor_types_765aa6431c012acb, []int{12} } func (m *Response) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1586,7 +1586,7 @@ func (m *ResponseException) Reset() { *m = ResponseException{} } func (m *ResponseException) String() string { return proto.CompactTextString(m) } func (*ResponseException) ProtoMessage() {} func (*ResponseException) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{13} + return fileDescriptor_types_765aa6431c012acb, []int{13} } func (m *ResponseException) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1633,7 +1633,7 @@ func (m *ResponseEcho) Reset() { *m = ResponseEcho{} } func (m *ResponseEcho) String() string { return proto.CompactTextString(m) } func (*ResponseEcho) ProtoMessage() {} func (*ResponseEcho) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{14} + return fileDescriptor_types_765aa6431c012acb, []int{14} } func (m *ResponseEcho) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1679,7 +1679,7 @@ func (m *ResponseFlush) Reset() { *m = ResponseFlush{} } func (m *ResponseFlush) String() string { return proto.CompactTextString(m) } func (*ResponseFlush) ProtoMessage() {} func (*ResponseFlush) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{15} + return fileDescriptor_types_765aa6431c012acb, []int{15} } func (m *ResponseFlush) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1723,7 +1723,7 @@ func (m *ResponseInfo) Reset() { *m = ResponseInfo{} } func (m *ResponseInfo) String() string { return proto.CompactTextString(m) } func (*ResponseInfo) ProtoMessage() {} func (*ResponseInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{16} + return fileDescriptor_types_765aa6431c012acb, []int{16} } func (m *ResponseInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1802,7 +1802,7 @@ func (m *ResponseSetOption) Reset() { *m = ResponseSetOption{} } func (m *ResponseSetOption) String() string { return proto.CompactTextString(m) } func (*ResponseSetOption) ProtoMessage() {} func (*ResponseSetOption) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{17} + return fileDescriptor_types_765aa6431c012acb, []int{17} } func (m *ResponseSetOption) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1864,7 +1864,7 @@ func (m *ResponseInitChain) Reset() { *m = ResponseInitChain{} } func (m *ResponseInitChain) String() string { return proto.CompactTextString(m) } func (*ResponseInitChain) ProtoMessage() {} func (*ResponseInitChain) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{18} + return fileDescriptor_types_765aa6431c012acb, []int{18} } func (m *ResponseInitChain) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -1927,7 +1927,7 @@ func (m *ResponseQuery) Reset() { *m = ResponseQuery{} } func (m *ResponseQuery) String() string { return proto.CompactTextString(m) } func (*ResponseQuery) ProtoMessage() {} func (*ResponseQuery) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{19} + return fileDescriptor_types_765aa6431c012acb, []int{19} } func (m *ResponseQuery) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2030,7 +2030,7 @@ func (m *ResponseBeginBlock) Reset() { *m = ResponseBeginBlock{} } func (m *ResponseBeginBlock) String() string { return proto.CompactTextString(m) } func (*ResponseBeginBlock) ProtoMessage() {} func (*ResponseBeginBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{20} + return fileDescriptor_types_765aa6431c012acb, []int{20} } func (m *ResponseBeginBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2066,6 +2066,53 @@ func (m *ResponseBeginBlock) GetEvents() []Event { return nil } +type ResponseBeginBlockDeprecated struct { + Tags []common.KVPair `protobuf:"bytes,1,rep,name=tags" json:"tags,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ResponseBeginBlockDeprecated) Reset() { *m = ResponseBeginBlockDeprecated{} } +func (m *ResponseBeginBlockDeprecated) String() string { return proto.CompactTextString(m) } +func (*ResponseBeginBlockDeprecated) ProtoMessage() {} +func (*ResponseBeginBlockDeprecated) Descriptor() ([]byte, []int) { + return fileDescriptor_types_765aa6431c012acb, []int{21} +} +func (m *ResponseBeginBlockDeprecated) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ResponseBeginBlockDeprecated) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ResponseBeginBlockDeprecated.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (dst *ResponseBeginBlockDeprecated) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResponseBeginBlockDeprecated.Merge(dst, src) +} +func (m *ResponseBeginBlockDeprecated) XXX_Size() int { + return m.Size() +} +func (m *ResponseBeginBlockDeprecated) XXX_DiscardUnknown() { + xxx_messageInfo_ResponseBeginBlockDeprecated.DiscardUnknown(m) +} + +var xxx_messageInfo_ResponseBeginBlockDeprecated proto.InternalMessageInfo + +func (m *ResponseBeginBlockDeprecated) GetTags() []common.KVPair { + if m != nil { + return m.Tags + } + return nil +} + type ResponseCheckTx struct { Code uint32 `protobuf:"varint,1,opt,name=code,proto3" json:"code,omitempty"` Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` @@ -2084,7 +2131,7 @@ func (m *ResponseCheckTx) Reset() { *m = ResponseCheckTx{} } func (m *ResponseCheckTx) String() string { return proto.CompactTextString(m) } func (*ResponseCheckTx) ProtoMessage() {} func (*ResponseCheckTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{21} + return fileDescriptor_types_765aa6431c012acb, []int{22} } func (m *ResponseCheckTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2187,7 +2234,7 @@ func (m *ResponseCheckTxDeprecated) Reset() { *m = ResponseCheckTxDeprec func (m *ResponseCheckTxDeprecated) String() string { return proto.CompactTextString(m) } func (*ResponseCheckTxDeprecated) ProtoMessage() {} func (*ResponseCheckTxDeprecated) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{22} + return fileDescriptor_types_765aa6431c012acb, []int{23} } func (m *ResponseCheckTxDeprecated) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2290,7 +2337,7 @@ func (m *ResponseDeliverTx) Reset() { *m = ResponseDeliverTx{} } func (m *ResponseDeliverTx) String() string { return proto.CompactTextString(m) } func (*ResponseDeliverTx) ProtoMessage() {} func (*ResponseDeliverTx) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{23} + return fileDescriptor_types_765aa6431c012acb, []int{24} } func (m *ResponseDeliverTx) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2393,7 +2440,7 @@ func (m *ResponseDeliverTxDeprecated) Reset() { *m = ResponseDeliverTxDe func (m *ResponseDeliverTxDeprecated) String() string { return proto.CompactTextString(m) } func (*ResponseDeliverTxDeprecated) ProtoMessage() {} func (*ResponseDeliverTxDeprecated) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{24} + return fileDescriptor_types_765aa6431c012acb, []int{25} } func (m *ResponseDeliverTxDeprecated) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2491,7 +2538,7 @@ func (m *ResponseEndBlock) Reset() { *m = ResponseEndBlock{} } func (m *ResponseEndBlock) String() string { return proto.CompactTextString(m) } func (*ResponseEndBlock) ProtoMessage() {} func (*ResponseEndBlock) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{25} + return fileDescriptor_types_765aa6431c012acb, []int{26} } func (m *ResponseEndBlock) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2541,6 +2588,69 @@ func (m *ResponseEndBlock) GetEvents() []Event { return nil } +type ResponseEndBlockDeprecated struct { + ValidatorUpdates []ValidatorUpdate `protobuf:"bytes,1,rep,name=validator_updates,json=validatorUpdates" json:"validator_updates"` + ConsensusParamUpdates *ConsensusParams `protobuf:"bytes,2,opt,name=consensus_param_updates,json=consensusParamUpdates" json:"consensus_param_updates,omitempty"` + Tags []common.KVPair `protobuf:"bytes,3,rep,name=tags" json:"tags,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *ResponseEndBlockDeprecated) Reset() { *m = ResponseEndBlockDeprecated{} } +func (m *ResponseEndBlockDeprecated) String() string { return proto.CompactTextString(m) } +func (*ResponseEndBlockDeprecated) ProtoMessage() {} +func (*ResponseEndBlockDeprecated) Descriptor() ([]byte, []int) { + return fileDescriptor_types_765aa6431c012acb, []int{27} +} +func (m *ResponseEndBlockDeprecated) XXX_Unmarshal(b []byte) error { + return m.Unmarshal(b) +} +func (m *ResponseEndBlockDeprecated) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + if deterministic { + return xxx_messageInfo_ResponseEndBlockDeprecated.Marshal(b, m, deterministic) + } else { + b = b[:cap(b)] + n, err := m.MarshalTo(b) + if err != nil { + return nil, err + } + return b[:n], nil + } +} +func (dst *ResponseEndBlockDeprecated) XXX_Merge(src proto.Message) { + xxx_messageInfo_ResponseEndBlockDeprecated.Merge(dst, src) +} +func (m *ResponseEndBlockDeprecated) XXX_Size() int { + return m.Size() +} +func (m *ResponseEndBlockDeprecated) XXX_DiscardUnknown() { + xxx_messageInfo_ResponseEndBlockDeprecated.DiscardUnknown(m) +} + +var xxx_messageInfo_ResponseEndBlockDeprecated proto.InternalMessageInfo + +func (m *ResponseEndBlockDeprecated) GetValidatorUpdates() []ValidatorUpdate { + if m != nil { + return m.ValidatorUpdates + } + return nil +} + +func (m *ResponseEndBlockDeprecated) GetConsensusParamUpdates() *ConsensusParams { + if m != nil { + return m.ConsensusParamUpdates + } + return nil +} + +func (m *ResponseEndBlockDeprecated) GetTags() []common.KVPair { + if m != nil { + return m.Tags + } + return nil +} + type ResponseCommit struct { // reserve 1 Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"` @@ -2553,7 +2663,7 @@ func (m *ResponseCommit) Reset() { *m = ResponseCommit{} } func (m *ResponseCommit) String() string { return proto.CompactTextString(m) } func (*ResponseCommit) ProtoMessage() {} func (*ResponseCommit) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{26} + return fileDescriptor_types_765aa6431c012acb, []int{28} } func (m *ResponseCommit) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2604,7 +2714,7 @@ func (m *ConsensusParams) Reset() { *m = ConsensusParams{} } func (m *ConsensusParams) String() string { return proto.CompactTextString(m) } func (*ConsensusParams) ProtoMessage() {} func (*ConsensusParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{27} + return fileDescriptor_types_765aa6431c012acb, []int{29} } func (m *ConsensusParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2668,7 +2778,7 @@ func (m *BlockSizeParams) Reset() { *m = BlockSizeParams{} } func (m *BlockSizeParams) String() string { return proto.CompactTextString(m) } func (*BlockSizeParams) ProtoMessage() {} func (*BlockSizeParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{28} + return fileDescriptor_types_765aa6431c012acb, []int{30} } func (m *BlockSizeParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2724,7 +2834,7 @@ func (m *EvidenceParams) Reset() { *m = EvidenceParams{} } func (m *EvidenceParams) String() string { return proto.CompactTextString(m) } func (*EvidenceParams) ProtoMessage() {} func (*EvidenceParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{29} + return fileDescriptor_types_765aa6431c012acb, []int{31} } func (m *EvidenceParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2772,7 +2882,7 @@ func (m *ValidatorParams) Reset() { *m = ValidatorParams{} } func (m *ValidatorParams) String() string { return proto.CompactTextString(m) } func (*ValidatorParams) ProtoMessage() {} func (*ValidatorParams) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{30} + return fileDescriptor_types_765aa6431c012acb, []int{32} } func (m *ValidatorParams) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2820,7 +2930,7 @@ func (m *LastCommitInfo) Reset() { *m = LastCommitInfo{} } func (m *LastCommitInfo) String() string { return proto.CompactTextString(m) } func (*LastCommitInfo) ProtoMessage() {} func (*LastCommitInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{31} + return fileDescriptor_types_765aa6431c012acb, []int{33} } func (m *LastCommitInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2875,7 +2985,7 @@ func (m *Event) Reset() { *m = Event{} } func (m *Event) String() string { return proto.CompactTextString(m) } func (*Event) ProtoMessage() {} func (*Event) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{32} + return fileDescriptor_types_765aa6431c012acb, []int{34} } func (m *Event) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -2949,7 +3059,7 @@ func (m *Header) Reset() { *m = Header{} } func (m *Header) String() string { return proto.CompactTextString(m) } func (*Header) ProtoMessage() {} func (*Header) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{33} + return fileDescriptor_types_765aa6431c012acb, []int{35} } func (m *Header) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3102,7 +3212,7 @@ func (m *Version) Reset() { *m = Version{} } func (m *Version) String() string { return proto.CompactTextString(m) } func (*Version) ProtoMessage() {} func (*Version) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{34} + return fileDescriptor_types_765aa6431c012acb, []int{36} } func (m *Version) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3157,7 +3267,7 @@ func (m *BlockID) Reset() { *m = BlockID{} } func (m *BlockID) String() string { return proto.CompactTextString(m) } func (*BlockID) ProtoMessage() {} func (*BlockID) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{35} + return fileDescriptor_types_765aa6431c012acb, []int{37} } func (m *BlockID) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3212,7 +3322,7 @@ func (m *PartSetHeader) Reset() { *m = PartSetHeader{} } func (m *PartSetHeader) String() string { return proto.CompactTextString(m) } func (*PartSetHeader) ProtoMessage() {} func (*PartSetHeader) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{36} + return fileDescriptor_types_765aa6431c012acb, []int{38} } func (m *PartSetHeader) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3269,7 +3379,7 @@ func (m *Validator) Reset() { *m = Validator{} } func (m *Validator) String() string { return proto.CompactTextString(m) } func (*Validator) ProtoMessage() {} func (*Validator) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{37} + return fileDescriptor_types_765aa6431c012acb, []int{39} } func (m *Validator) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3325,7 +3435,7 @@ func (m *ValidatorUpdate) Reset() { *m = ValidatorUpdate{} } func (m *ValidatorUpdate) String() string { return proto.CompactTextString(m) } func (*ValidatorUpdate) ProtoMessage() {} func (*ValidatorUpdate) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{38} + return fileDescriptor_types_765aa6431c012acb, []int{40} } func (m *ValidatorUpdate) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3381,7 +3491,7 @@ func (m *VoteInfo) Reset() { *m = VoteInfo{} } func (m *VoteInfo) String() string { return proto.CompactTextString(m) } func (*VoteInfo) ProtoMessage() {} func (*VoteInfo) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{39} + return fileDescriptor_types_765aa6431c012acb, []int{41} } func (m *VoteInfo) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3436,7 +3546,7 @@ func (m *PubKey) Reset() { *m = PubKey{} } func (m *PubKey) String() string { return proto.CompactTextString(m) } func (*PubKey) ProtoMessage() {} func (*PubKey) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{40} + return fileDescriptor_types_765aa6431c012acb, []int{42} } func (m *PubKey) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3494,7 +3604,7 @@ func (m *Evidence) Reset() { *m = Evidence{} } func (m *Evidence) String() string { return proto.CompactTextString(m) } func (*Evidence) ProtoMessage() {} func (*Evidence) Descriptor() ([]byte, []int) { - return fileDescriptor_types_7fd6485bbc04efd7, []int{41} + return fileDescriptor_types_765aa6431c012acb, []int{43} } func (m *Evidence) XXX_Unmarshal(b []byte) error { return m.Unmarshal(b) @@ -3601,6 +3711,8 @@ func init() { golang_proto.RegisterType((*ResponseQuery)(nil), "types.ResponseQuery") proto.RegisterType((*ResponseBeginBlock)(nil), "types.ResponseBeginBlock") golang_proto.RegisterType((*ResponseBeginBlock)(nil), "types.ResponseBeginBlock") + proto.RegisterType((*ResponseBeginBlockDeprecated)(nil), "types.ResponseBeginBlockDeprecated") + golang_proto.RegisterType((*ResponseBeginBlockDeprecated)(nil), "types.ResponseBeginBlockDeprecated") proto.RegisterType((*ResponseCheckTx)(nil), "types.ResponseCheckTx") golang_proto.RegisterType((*ResponseCheckTx)(nil), "types.ResponseCheckTx") proto.RegisterType((*ResponseCheckTxDeprecated)(nil), "types.ResponseCheckTxDeprecated") @@ -3611,6 +3723,8 @@ func init() { golang_proto.RegisterType((*ResponseDeliverTxDeprecated)(nil), "types.ResponseDeliverTxDeprecated") proto.RegisterType((*ResponseEndBlock)(nil), "types.ResponseEndBlock") golang_proto.RegisterType((*ResponseEndBlock)(nil), "types.ResponseEndBlock") + proto.RegisterType((*ResponseEndBlockDeprecated)(nil), "types.ResponseEndBlockDeprecated") + golang_proto.RegisterType((*ResponseEndBlockDeprecated)(nil), "types.ResponseEndBlockDeprecated") proto.RegisterType((*ResponseCommit)(nil), "types.ResponseCommit") golang_proto.RegisterType((*ResponseCommit)(nil), "types.ResponseCommit") proto.RegisterType((*ConsensusParams)(nil), "types.ConsensusParams") @@ -4875,6 +4989,38 @@ func (this *ResponseBeginBlock) Equal(that interface{}) bool { } return true } +func (this *ResponseBeginBlockDeprecated) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*ResponseBeginBlockDeprecated) + if !ok { + that2, ok := that.(ResponseBeginBlockDeprecated) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if len(this.Tags) != len(that1.Tags) { + return false + } + for i := range this.Tags { + if !this.Tags[i].Equal(&that1.Tags[i]) { + return false + } + } + if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { + return false + } + return true +} func (this *ResponseCheckTx) Equal(that interface{}) bool { if that == nil { return this == nil @@ -5130,6 +5276,49 @@ func (this *ResponseEndBlock) Equal(that interface{}) bool { } return true } +func (this *ResponseEndBlockDeprecated) Equal(that interface{}) bool { + if that == nil { + return this == nil + } + + that1, ok := that.(*ResponseEndBlockDeprecated) + if !ok { + that2, ok := that.(ResponseEndBlockDeprecated) + if ok { + that1 = &that2 + } else { + return false + } + } + if that1 == nil { + return this == nil + } else if this == nil { + return false + } + if len(this.ValidatorUpdates) != len(that1.ValidatorUpdates) { + return false + } + for i := range this.ValidatorUpdates { + if !this.ValidatorUpdates[i].Equal(&that1.ValidatorUpdates[i]) { + return false + } + } + if !this.ConsensusParamUpdates.Equal(that1.ConsensusParamUpdates) { + return false + } + if len(this.Tags) != len(that1.Tags) { + return false + } + for i := range this.Tags { + if !this.Tags[i].Equal(&that1.Tags[i]) { + return false + } + } + if !bytes.Equal(this.XXX_unrecognized, that1.XXX_unrecognized) { + return false + } + return true +} func (this *ResponseCommit) Equal(that interface{}) bool { if that == nil { return this == nil @@ -7157,6 +7346,39 @@ func (m *ResponseBeginBlock) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *ResponseBeginBlockDeprecated) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResponseBeginBlockDeprecated) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.Tags) > 0 { + for _, msg := range m.Tags { + dAtA[i] = 0xa + i++ + i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.XXX_unrecognized != nil { + i += copy(dAtA[i:], m.XXX_unrecognized) + } + return i, nil +} + func (m *ResponseCheckTx) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -7500,6 +7722,61 @@ func (m *ResponseEndBlock) MarshalTo(dAtA []byte) (int, error) { return i, nil } +func (m *ResponseEndBlockDeprecated) Marshal() (dAtA []byte, err error) { + size := m.Size() + dAtA = make([]byte, size) + n, err := m.MarshalTo(dAtA) + if err != nil { + return nil, err + } + return dAtA[:n], nil +} + +func (m *ResponseEndBlockDeprecated) MarshalTo(dAtA []byte) (int, error) { + var i int + _ = i + var l int + _ = l + if len(m.ValidatorUpdates) > 0 { + for _, msg := range m.ValidatorUpdates { + dAtA[i] = 0xa + i++ + i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.ConsensusParamUpdates != nil { + dAtA[i] = 0x12 + i++ + i = encodeVarintTypes(dAtA, i, uint64(m.ConsensusParamUpdates.Size())) + n33, err := m.ConsensusParamUpdates.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n33 + } + if len(m.Tags) > 0 { + for _, msg := range m.Tags { + dAtA[i] = 0x1a + i++ + i = encodeVarintTypes(dAtA, i, uint64(msg.Size())) + n, err := msg.MarshalTo(dAtA[i:]) + if err != nil { + return 0, err + } + i += n + } + } + if m.XXX_unrecognized != nil { + i += copy(dAtA[i:], m.XXX_unrecognized) + } + return i, nil +} + func (m *ResponseCommit) Marshal() (dAtA []byte, err error) { size := m.Size() dAtA = make([]byte, size) @@ -7546,31 +7823,31 @@ func (m *ConsensusParams) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0xa i++ i = encodeVarintTypes(dAtA, i, uint64(m.BlockSize.Size())) - n33, err := m.BlockSize.MarshalTo(dAtA[i:]) + n34, err := m.BlockSize.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n33 + i += n34 } if m.Evidence != nil { dAtA[i] = 0x12 i++ i = encodeVarintTypes(dAtA, i, uint64(m.Evidence.Size())) - n34, err := m.Evidence.MarshalTo(dAtA[i:]) + n35, err := m.Evidence.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n34 + i += n35 } if m.Validator != nil { dAtA[i] = 0x1a i++ i = encodeVarintTypes(dAtA, i, uint64(m.Validator.Size())) - n35, err := m.Validator.MarshalTo(dAtA[i:]) + n36, err := m.Validator.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n35 + i += n36 } if m.XXX_unrecognized != nil { i += copy(dAtA[i:], m.XXX_unrecognized) @@ -7766,11 +8043,11 @@ func (m *Header) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0xa i++ i = encodeVarintTypes(dAtA, i, uint64(m.Version.Size())) - n36, err := m.Version.MarshalTo(dAtA[i:]) + n37, err := m.Version.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n36 + i += n37 if len(m.ChainID) > 0 { dAtA[i] = 0x12 i++ @@ -7785,11 +8062,11 @@ func (m *Header) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0x22 i++ i = encodeVarintTypes(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.Time))) - n37, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i:]) + n38, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i:]) if err != nil { return 0, err } - i += n37 + i += n38 if m.NumTxs != 0 { dAtA[i] = 0x28 i++ @@ -7803,11 +8080,11 @@ func (m *Header) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0x3a i++ i = encodeVarintTypes(dAtA, i, uint64(m.LastBlockId.Size())) - n38, err := m.LastBlockId.MarshalTo(dAtA[i:]) + n39, err := m.LastBlockId.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n38 + i += n39 if len(m.LastCommitHash) > 0 { dAtA[i] = 0x42 i++ @@ -7925,11 +8202,11 @@ func (m *BlockID) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0x12 i++ i = encodeVarintTypes(dAtA, i, uint64(m.PartsHeader.Size())) - n39, err := m.PartsHeader.MarshalTo(dAtA[i:]) + n40, err := m.PartsHeader.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n39 + i += n40 if m.XXX_unrecognized != nil { i += copy(dAtA[i:], m.XXX_unrecognized) } @@ -8018,11 +8295,11 @@ func (m *ValidatorUpdate) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0xa i++ i = encodeVarintTypes(dAtA, i, uint64(m.PubKey.Size())) - n40, err := m.PubKey.MarshalTo(dAtA[i:]) + n41, err := m.PubKey.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n40 + i += n41 if m.Power != 0 { dAtA[i] = 0x10 i++ @@ -8052,11 +8329,11 @@ func (m *VoteInfo) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0xa i++ i = encodeVarintTypes(dAtA, i, uint64(m.Validator.Size())) - n41, err := m.Validator.MarshalTo(dAtA[i:]) + n42, err := m.Validator.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n41 + i += n42 if m.SignedLastBlock { dAtA[i] = 0x10 i++ @@ -8130,11 +8407,11 @@ func (m *Evidence) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0x12 i++ i = encodeVarintTypes(dAtA, i, uint64(m.Validator.Size())) - n42, err := m.Validator.MarshalTo(dAtA[i:]) + n43, err := m.Validator.MarshalTo(dAtA[i:]) if err != nil { return 0, err } - i += n42 + i += n43 if m.Height != 0 { dAtA[i] = 0x18 i++ @@ -8143,11 +8420,11 @@ func (m *Evidence) MarshalTo(dAtA []byte) (int, error) { dAtA[i] = 0x22 i++ i = encodeVarintTypes(dAtA, i, uint64(github_com_gogo_protobuf_types.SizeOfStdTime(m.Time))) - n43, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i:]) + n44, err := github_com_gogo_protobuf_types.StdTimeMarshalTo(m.Time, dAtA[i:]) if err != nil { return 0, err } - i += n43 + i += n44 if m.TotalVotingPower != 0 { dAtA[i] = 0x28 i++ @@ -8632,12 +8909,28 @@ func NewPopulatedResponseBeginBlock(r randyTypes, easy bool) *ResponseBeginBlock return this } +func NewPopulatedResponseBeginBlockDeprecated(r randyTypes, easy bool) *ResponseBeginBlockDeprecated { + this := &ResponseBeginBlockDeprecated{} + if r.Intn(10) != 0 { + v20 := r.Intn(5) + this.Tags = make([]common.KVPair, v20) + for i := 0; i < v20; i++ { + v21 := common.NewPopulatedKVPair(r, easy) + this.Tags[i] = *v21 + } + } + if !easy && r.Intn(10) != 0 { + this.XXX_unrecognized = randUnrecognizedTypes(r, 2) + } + return this +} + func NewPopulatedResponseCheckTx(r randyTypes, easy bool) *ResponseCheckTx { this := &ResponseCheckTx{} this.Code = uint32(r.Uint32()) - v20 := r.Intn(100) - this.Data = make([]byte, v20) - for i := 0; i < v20; i++ { + v22 := r.Intn(100) + this.Data = make([]byte, v22) + for i := 0; i < v22; i++ { this.Data[i] = byte(r.Intn(256)) } this.Log = string(randStringTypes(r)) @@ -8651,11 +8944,11 @@ func NewPopulatedResponseCheckTx(r randyTypes, easy bool) *ResponseCheckTx { this.GasUsed *= -1 } if r.Intn(10) != 0 { - v21 := r.Intn(5) - this.Events = make([]Event, v21) - for i := 0; i < v21; i++ { - v22 := NewPopulatedEvent(r, easy) - this.Events[i] = *v22 + v23 := r.Intn(5) + this.Events = make([]Event, v23) + for i := 0; i < v23; i++ { + v24 := NewPopulatedEvent(r, easy) + this.Events[i] = *v24 } } this.Codespace = string(randStringTypes(r)) @@ -8668,9 +8961,9 @@ func NewPopulatedResponseCheckTx(r randyTypes, easy bool) *ResponseCheckTx { func NewPopulatedResponseCheckTxDeprecated(r randyTypes, easy bool) *ResponseCheckTxDeprecated { this := &ResponseCheckTxDeprecated{} this.Code = uint32(r.Uint32()) - v23 := r.Intn(100) - this.Data = make([]byte, v23) - for i := 0; i < v23; i++ { + v25 := r.Intn(100) + this.Data = make([]byte, v25) + for i := 0; i < v25; i++ { this.Data[i] = byte(r.Intn(256)) } this.Log = string(randStringTypes(r)) @@ -8684,11 +8977,11 @@ func NewPopulatedResponseCheckTxDeprecated(r randyTypes, easy bool) *ResponseChe this.GasUsed *= -1 } if r.Intn(10) != 0 { - v24 := r.Intn(5) - this.Tags = make([]common.KVPair, v24) - for i := 0; i < v24; i++ { - v25 := common.NewPopulatedKVPair(r, easy) - this.Tags[i] = *v25 + v26 := r.Intn(5) + this.Tags = make([]common.KVPair, v26) + for i := 0; i < v26; i++ { + v27 := common.NewPopulatedKVPair(r, easy) + this.Tags[i] = *v27 } } this.Codespace = string(randStringTypes(r)) @@ -8701,9 +8994,9 @@ func NewPopulatedResponseCheckTxDeprecated(r randyTypes, easy bool) *ResponseChe func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { this := &ResponseDeliverTx{} this.Code = uint32(r.Uint32()) - v26 := r.Intn(100) - this.Data = make([]byte, v26) - for i := 0; i < v26; i++ { + v28 := r.Intn(100) + this.Data = make([]byte, v28) + for i := 0; i < v28; i++ { this.Data[i] = byte(r.Intn(256)) } this.Log = string(randStringTypes(r)) @@ -8717,11 +9010,11 @@ func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { this.GasUsed *= -1 } if r.Intn(10) != 0 { - v27 := r.Intn(5) - this.Events = make([]Event, v27) - for i := 0; i < v27; i++ { - v28 := NewPopulatedEvent(r, easy) - this.Events[i] = *v28 + v29 := r.Intn(5) + this.Events = make([]Event, v29) + for i := 0; i < v29; i++ { + v30 := NewPopulatedEvent(r, easy) + this.Events[i] = *v30 } } this.Codespace = string(randStringTypes(r)) @@ -8734,9 +9027,9 @@ func NewPopulatedResponseDeliverTx(r randyTypes, easy bool) *ResponseDeliverTx { func NewPopulatedResponseDeliverTxDeprecated(r randyTypes, easy bool) *ResponseDeliverTxDeprecated { this := &ResponseDeliverTxDeprecated{} this.Code = uint32(r.Uint32()) - v29 := r.Intn(100) - this.Data = make([]byte, v29) - for i := 0; i < v29; i++ { + v31 := r.Intn(100) + this.Data = make([]byte, v31) + for i := 0; i < v31; i++ { this.Data[i] = byte(r.Intn(256)) } this.Log = string(randStringTypes(r)) @@ -8750,11 +9043,11 @@ func NewPopulatedResponseDeliverTxDeprecated(r randyTypes, easy bool) *ResponseD this.GasUsed *= -1 } if r.Intn(10) != 0 { - v30 := r.Intn(5) - this.Tags = make([]common.KVPair, v30) - for i := 0; i < v30; i++ { - v31 := common.NewPopulatedKVPair(r, easy) - this.Tags[i] = *v31 + v32 := r.Intn(5) + this.Tags = make([]common.KVPair, v32) + for i := 0; i < v32; i++ { + v33 := common.NewPopulatedKVPair(r, easy) + this.Tags[i] = *v33 } } this.Codespace = string(randStringTypes(r)) @@ -8767,22 +9060,49 @@ func NewPopulatedResponseDeliverTxDeprecated(r randyTypes, easy bool) *ResponseD func NewPopulatedResponseEndBlock(r randyTypes, easy bool) *ResponseEndBlock { this := &ResponseEndBlock{} if r.Intn(10) != 0 { - v32 := r.Intn(5) - this.ValidatorUpdates = make([]ValidatorUpdate, v32) - for i := 0; i < v32; i++ { - v33 := NewPopulatedValidatorUpdate(r, easy) - this.ValidatorUpdates[i] = *v33 + v34 := r.Intn(5) + this.ValidatorUpdates = make([]ValidatorUpdate, v34) + for i := 0; i < v34; i++ { + v35 := NewPopulatedValidatorUpdate(r, easy) + this.ValidatorUpdates[i] = *v35 } } if r.Intn(10) != 0 { this.ConsensusParamUpdates = NewPopulatedConsensusParams(r, easy) } if r.Intn(10) != 0 { - v34 := r.Intn(5) - this.Events = make([]Event, v34) - for i := 0; i < v34; i++ { - v35 := NewPopulatedEvent(r, easy) - this.Events[i] = *v35 + v36 := r.Intn(5) + this.Events = make([]Event, v36) + for i := 0; i < v36; i++ { + v37 := NewPopulatedEvent(r, easy) + this.Events[i] = *v37 + } + } + if !easy && r.Intn(10) != 0 { + this.XXX_unrecognized = randUnrecognizedTypes(r, 4) + } + return this +} + +func NewPopulatedResponseEndBlockDeprecated(r randyTypes, easy bool) *ResponseEndBlockDeprecated { + this := &ResponseEndBlockDeprecated{} + if r.Intn(10) != 0 { + v38 := r.Intn(5) + this.ValidatorUpdates = make([]ValidatorUpdate, v38) + for i := 0; i < v38; i++ { + v39 := NewPopulatedValidatorUpdate(r, easy) + this.ValidatorUpdates[i] = *v39 + } + } + if r.Intn(10) != 0 { + this.ConsensusParamUpdates = NewPopulatedConsensusParams(r, easy) + } + if r.Intn(10) != 0 { + v40 := r.Intn(5) + this.Tags = make([]common.KVPair, v40) + for i := 0; i < v40; i++ { + v41 := common.NewPopulatedKVPair(r, easy) + this.Tags[i] = *v41 } } if !easy && r.Intn(10) != 0 { @@ -8793,9 +9113,9 @@ func NewPopulatedResponseEndBlock(r randyTypes, easy bool) *ResponseEndBlock { func NewPopulatedResponseCommit(r randyTypes, easy bool) *ResponseCommit { this := &ResponseCommit{} - v36 := r.Intn(100) - this.Data = make([]byte, v36) - for i := 0; i < v36; i++ { + v42 := r.Intn(100) + this.Data = make([]byte, v42) + for i := 0; i < v42; i++ { this.Data[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8851,9 +9171,9 @@ func NewPopulatedEvidenceParams(r randyTypes, easy bool) *EvidenceParams { func NewPopulatedValidatorParams(r randyTypes, easy bool) *ValidatorParams { this := &ValidatorParams{} - v37 := r.Intn(10) - this.PubKeyTypes = make([]string, v37) - for i := 0; i < v37; i++ { + v43 := r.Intn(10) + this.PubKeyTypes = make([]string, v43) + for i := 0; i < v43; i++ { this.PubKeyTypes[i] = string(randStringTypes(r)) } if !easy && r.Intn(10) != 0 { @@ -8869,11 +9189,11 @@ func NewPopulatedLastCommitInfo(r randyTypes, easy bool) *LastCommitInfo { this.Round *= -1 } if r.Intn(10) != 0 { - v38 := r.Intn(5) - this.Votes = make([]VoteInfo, v38) - for i := 0; i < v38; i++ { - v39 := NewPopulatedVoteInfo(r, easy) - this.Votes[i] = *v39 + v44 := r.Intn(5) + this.Votes = make([]VoteInfo, v44) + for i := 0; i < v44; i++ { + v45 := NewPopulatedVoteInfo(r, easy) + this.Votes[i] = *v45 } } if !easy && r.Intn(10) != 0 { @@ -8886,11 +9206,11 @@ func NewPopulatedEvent(r randyTypes, easy bool) *Event { this := &Event{} this.Type = string(randStringTypes(r)) if r.Intn(10) != 0 { - v40 := r.Intn(5) - this.Attributes = make([]common.KVPair, v40) - for i := 0; i < v40; i++ { - v41 := common.NewPopulatedKVPair(r, easy) - this.Attributes[i] = *v41 + v46 := r.Intn(5) + this.Attributes = make([]common.KVPair, v46) + for i := 0; i < v46; i++ { + v47 := common.NewPopulatedKVPair(r, easy) + this.Attributes[i] = *v47 } } if !easy && r.Intn(10) != 0 { @@ -8901,15 +9221,15 @@ func NewPopulatedEvent(r randyTypes, easy bool) *Event { func NewPopulatedHeader(r randyTypes, easy bool) *Header { this := &Header{} - v42 := NewPopulatedVersion(r, easy) - this.Version = *v42 + v48 := NewPopulatedVersion(r, easy) + this.Version = *v48 this.ChainID = string(randStringTypes(r)) this.Height = int64(r.Int63()) if r.Intn(2) == 0 { this.Height *= -1 } - v43 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v43 + v49 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v49 this.NumTxs = int64(r.Int63()) if r.Intn(2) == 0 { this.NumTxs *= -1 @@ -8918,51 +9238,51 @@ func NewPopulatedHeader(r randyTypes, easy bool) *Header { if r.Intn(2) == 0 { this.TotalTxs *= -1 } - v44 := NewPopulatedBlockID(r, easy) - this.LastBlockId = *v44 - v45 := r.Intn(100) - this.LastCommitHash = make([]byte, v45) - for i := 0; i < v45; i++ { + v50 := NewPopulatedBlockID(r, easy) + this.LastBlockId = *v50 + v51 := r.Intn(100) + this.LastCommitHash = make([]byte, v51) + for i := 0; i < v51; i++ { this.LastCommitHash[i] = byte(r.Intn(256)) } - v46 := r.Intn(100) - this.DataHash = make([]byte, v46) - for i := 0; i < v46; i++ { + v52 := r.Intn(100) + this.DataHash = make([]byte, v52) + for i := 0; i < v52; i++ { this.DataHash[i] = byte(r.Intn(256)) } - v47 := r.Intn(100) - this.ValidatorsHash = make([]byte, v47) - for i := 0; i < v47; i++ { + v53 := r.Intn(100) + this.ValidatorsHash = make([]byte, v53) + for i := 0; i < v53; i++ { this.ValidatorsHash[i] = byte(r.Intn(256)) } - v48 := r.Intn(100) - this.NextValidatorsHash = make([]byte, v48) - for i := 0; i < v48; i++ { + v54 := r.Intn(100) + this.NextValidatorsHash = make([]byte, v54) + for i := 0; i < v54; i++ { this.NextValidatorsHash[i] = byte(r.Intn(256)) } - v49 := r.Intn(100) - this.ConsensusHash = make([]byte, v49) - for i := 0; i < v49; i++ { + v55 := r.Intn(100) + this.ConsensusHash = make([]byte, v55) + for i := 0; i < v55; i++ { this.ConsensusHash[i] = byte(r.Intn(256)) } - v50 := r.Intn(100) - this.AppHash = make([]byte, v50) - for i := 0; i < v50; i++ { + v56 := r.Intn(100) + this.AppHash = make([]byte, v56) + for i := 0; i < v56; i++ { this.AppHash[i] = byte(r.Intn(256)) } - v51 := r.Intn(100) - this.LastResultsHash = make([]byte, v51) - for i := 0; i < v51; i++ { + v57 := r.Intn(100) + this.LastResultsHash = make([]byte, v57) + for i := 0; i < v57; i++ { this.LastResultsHash[i] = byte(r.Intn(256)) } - v52 := r.Intn(100) - this.EvidenceHash = make([]byte, v52) - for i := 0; i < v52; i++ { + v58 := r.Intn(100) + this.EvidenceHash = make([]byte, v58) + for i := 0; i < v58; i++ { this.EvidenceHash[i] = byte(r.Intn(256)) } - v53 := r.Intn(100) - this.ProposerAddress = make([]byte, v53) - for i := 0; i < v53; i++ { + v59 := r.Intn(100) + this.ProposerAddress = make([]byte, v59) + for i := 0; i < v59; i++ { this.ProposerAddress[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -8983,13 +9303,13 @@ func NewPopulatedVersion(r randyTypes, easy bool) *Version { func NewPopulatedBlockID(r randyTypes, easy bool) *BlockID { this := &BlockID{} - v54 := r.Intn(100) - this.Hash = make([]byte, v54) - for i := 0; i < v54; i++ { + v60 := r.Intn(100) + this.Hash = make([]byte, v60) + for i := 0; i < v60; i++ { this.Hash[i] = byte(r.Intn(256)) } - v55 := NewPopulatedPartSetHeader(r, easy) - this.PartsHeader = *v55 + v61 := NewPopulatedPartSetHeader(r, easy) + this.PartsHeader = *v61 if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedTypes(r, 3) } @@ -9002,9 +9322,9 @@ func NewPopulatedPartSetHeader(r randyTypes, easy bool) *PartSetHeader { if r.Intn(2) == 0 { this.Total *= -1 } - v56 := r.Intn(100) - this.Hash = make([]byte, v56) - for i := 0; i < v56; i++ { + v62 := r.Intn(100) + this.Hash = make([]byte, v62) + for i := 0; i < v62; i++ { this.Hash[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -9015,9 +9335,9 @@ func NewPopulatedPartSetHeader(r randyTypes, easy bool) *PartSetHeader { func NewPopulatedValidator(r randyTypes, easy bool) *Validator { this := &Validator{} - v57 := r.Intn(100) - this.Address = make([]byte, v57) - for i := 0; i < v57; i++ { + v63 := r.Intn(100) + this.Address = make([]byte, v63) + for i := 0; i < v63; i++ { this.Address[i] = byte(r.Intn(256)) } this.Power = int64(r.Int63()) @@ -9032,8 +9352,8 @@ func NewPopulatedValidator(r randyTypes, easy bool) *Validator { func NewPopulatedValidatorUpdate(r randyTypes, easy bool) *ValidatorUpdate { this := &ValidatorUpdate{} - v58 := NewPopulatedPubKey(r, easy) - this.PubKey = *v58 + v64 := NewPopulatedPubKey(r, easy) + this.PubKey = *v64 this.Power = int64(r.Int63()) if r.Intn(2) == 0 { this.Power *= -1 @@ -9046,8 +9366,8 @@ func NewPopulatedValidatorUpdate(r randyTypes, easy bool) *ValidatorUpdate { func NewPopulatedVoteInfo(r randyTypes, easy bool) *VoteInfo { this := &VoteInfo{} - v59 := NewPopulatedValidator(r, easy) - this.Validator = *v59 + v65 := NewPopulatedValidator(r, easy) + this.Validator = *v65 this.SignedLastBlock = bool(bool(r.Intn(2) == 0)) if !easy && r.Intn(10) != 0 { this.XXX_unrecognized = randUnrecognizedTypes(r, 3) @@ -9058,9 +9378,9 @@ func NewPopulatedVoteInfo(r randyTypes, easy bool) *VoteInfo { func NewPopulatedPubKey(r randyTypes, easy bool) *PubKey { this := &PubKey{} this.Type = string(randStringTypes(r)) - v60 := r.Intn(100) - this.Data = make([]byte, v60) - for i := 0; i < v60; i++ { + v66 := r.Intn(100) + this.Data = make([]byte, v66) + for i := 0; i < v66; i++ { this.Data[i] = byte(r.Intn(256)) } if !easy && r.Intn(10) != 0 { @@ -9072,14 +9392,14 @@ func NewPopulatedPubKey(r randyTypes, easy bool) *PubKey { func NewPopulatedEvidence(r randyTypes, easy bool) *Evidence { this := &Evidence{} this.Type = string(randStringTypes(r)) - v61 := NewPopulatedValidator(r, easy) - this.Validator = *v61 + v67 := NewPopulatedValidator(r, easy) + this.Validator = *v67 this.Height = int64(r.Int63()) if r.Intn(2) == 0 { this.Height *= -1 } - v62 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) - this.Time = *v62 + v68 := github_com_gogo_protobuf_types.NewPopulatedStdTime(r, easy) + this.Time = *v68 this.TotalVotingPower = int64(r.Int63()) if r.Intn(2) == 0 { this.TotalVotingPower *= -1 @@ -9109,9 +9429,9 @@ func randUTF8RuneTypes(r randyTypes) rune { return rune(ru + 61) } func randStringTypes(r randyTypes) string { - v63 := r.Intn(100) - tmps := make([]rune, v63) - for i := 0; i < v63; i++ { + v69 := r.Intn(100) + tmps := make([]rune, v69) + for i := 0; i < v69; i++ { tmps[i] = randUTF8RuneTypes(r) } return string(tmps) @@ -9133,11 +9453,11 @@ func randFieldTypes(dAtA []byte, r randyTypes, fieldNumber int, wire int) []byte switch wire { case 0: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) - v64 := r.Int63() + v70 := r.Int63() if r.Intn(2) == 0 { - v64 *= -1 + v70 *= -1 } - dAtA = encodeVarintPopulateTypes(dAtA, uint64(v64)) + dAtA = encodeVarintPopulateTypes(dAtA, uint64(v70)) case 1: dAtA = encodeVarintPopulateTypes(dAtA, uint64(key)) dAtA = append(dAtA, byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256)), byte(r.Intn(256))) @@ -9866,6 +10186,24 @@ func (m *ResponseBeginBlock) Size() (n int) { return n } +func (m *ResponseBeginBlockDeprecated) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.Tags) > 0 { + for _, e := range m.Tags { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + func (m *ResponseCheckTx) Size() (n int) { if m == nil { return 0 @@ -10066,6 +10404,34 @@ func (m *ResponseEndBlock) Size() (n int) { return n } +func (m *ResponseEndBlockDeprecated) Size() (n int) { + if m == nil { + return 0 + } + var l int + _ = l + if len(m.ValidatorUpdates) > 0 { + for _, e := range m.ValidatorUpdates { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if m.ConsensusParamUpdates != nil { + l = m.ConsensusParamUpdates.Size() + n += 1 + l + sovTypes(uint64(l)) + } + if len(m.Tags) > 0 { + for _, e := range m.Tags { + l = e.Size() + n += 1 + l + sovTypes(uint64(l)) + } + } + if m.XXX_unrecognized != nil { + n += len(m.XXX_unrecognized) + } + return n +} + func (m *ResponseCommit) Size() (n int) { if m == nil { return 0 @@ -13466,6 +13832,88 @@ func (m *ResponseBeginBlock) Unmarshal(dAtA []byte) error { } return nil } +func (m *ResponseBeginBlockDeprecated) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResponseBeginBlockDeprecated: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResponseBeginBlockDeprecated: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Tags = append(m.Tags, common.KVPair{}) + if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *ResponseCheckTx) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -14640,6 +15088,152 @@ func (m *ResponseEndBlock) Unmarshal(dAtA []byte) error { } return nil } +func (m *ResponseEndBlockDeprecated) Unmarshal(dAtA []byte) error { + l := len(dAtA) + iNdEx := 0 + for iNdEx < l { + preIndex := iNdEx + var wire uint64 + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + wire |= (uint64(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + fieldNum := int32(wire >> 3) + wireType := int(wire & 0x7) + if wireType == 4 { + return fmt.Errorf("proto: ResponseEndBlockDeprecated: wiretype end group for non-group") + } + if fieldNum <= 0 { + return fmt.Errorf("proto: ResponseEndBlockDeprecated: illegal tag %d (wire type %d)", fieldNum, wire) + } + switch fieldNum { + case 1: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ValidatorUpdates", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.ValidatorUpdates = append(m.ValidatorUpdates, ValidatorUpdate{}) + if err := m.ValidatorUpdates[len(m.ValidatorUpdates)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 2: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field ConsensusParamUpdates", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + if m.ConsensusParamUpdates == nil { + m.ConsensusParamUpdates = &ConsensusParams{} + } + if err := m.ConsensusParamUpdates.Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + case 3: + if wireType != 2 { + return fmt.Errorf("proto: wrong wireType = %d for field Tags", wireType) + } + var msglen int + for shift := uint(0); ; shift += 7 { + if shift >= 64 { + return ErrIntOverflowTypes + } + if iNdEx >= l { + return io.ErrUnexpectedEOF + } + b := dAtA[iNdEx] + iNdEx++ + msglen |= (int(b) & 0x7F) << shift + if b < 0x80 { + break + } + } + if msglen < 0 { + return ErrInvalidLengthTypes + } + postIndex := iNdEx + msglen + if postIndex > l { + return io.ErrUnexpectedEOF + } + m.Tags = append(m.Tags, common.KVPair{}) + if err := m.Tags[len(m.Tags)-1].Unmarshal(dAtA[iNdEx:postIndex]); err != nil { + return err + } + iNdEx = postIndex + default: + iNdEx = preIndex + skippy, err := skipTypes(dAtA[iNdEx:]) + if err != nil { + return err + } + if skippy < 0 { + return ErrInvalidLengthTypes + } + if (iNdEx + skippy) > l { + return io.ErrUnexpectedEOF + } + m.XXX_unrecognized = append(m.XXX_unrecognized, dAtA[iNdEx:iNdEx+skippy]...) + iNdEx += skippy + } + } + + if iNdEx > l { + return io.ErrUnexpectedEOF + } + return nil +} func (m *ResponseCommit) Unmarshal(dAtA []byte) error { l := len(dAtA) iNdEx := 0 @@ -16827,158 +17421,161 @@ var ( ErrIntOverflowTypes = fmt.Errorf("proto: integer overflow") ) -func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7fd6485bbc04efd7) } +func init() { proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_765aa6431c012acb) } func init() { - golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_7fd6485bbc04efd7) -} - -var fileDescriptor_types_7fd6485bbc04efd7 = []byte{ - // 2350 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x59, 0xcd, 0x73, 0x1c, 0x57, - 0x11, 0xd7, 0x68, 0x3f, 0xa7, 0x57, 0xfb, 0xe1, 0x67, 0xd9, 0x5e, 0x6f, 0x8c, 0xe4, 0x1a, 0x83, - 0x63, 0xc5, 0xce, 0x2a, 0x51, 0x30, 0x25, 0xc7, 0x21, 0x55, 0x5a, 0xdb, 0x44, 0xaa, 0x98, 0x20, - 0xc6, 0xb6, 0xb8, 0x50, 0x35, 0xf5, 0x76, 0xe7, 0x79, 0x77, 0xca, 0xbb, 0x33, 0x93, 0x99, 0xb7, - 0xf2, 0xca, 0x47, 0xce, 0x39, 0xe4, 0xc0, 0x1f, 0xc1, 0x95, 0x5b, 0x8e, 0x9c, 0xa8, 0x1c, 0x39, - 0x70, 0x36, 0x20, 0x8a, 0x0b, 0x55, 0x9c, 0x81, 0x03, 0x05, 0xf5, 0xfa, 0xbd, 0xf9, 0xd4, 0xac, - 0x93, 0x18, 0x0e, 0x54, 0xe5, 0x22, 0xcd, 0x7b, 0xdd, 0xfd, 0x3e, 0xfa, 0x75, 0xf7, 0xaf, 0xbb, - 0x17, 0x2e, 0xd2, 0xe1, 0xc8, 0xd9, 0xe6, 0x27, 0x3e, 0x0b, 0xe5, 0xdf, 0xbe, 0x1f, 0x78, 0xdc, - 0x23, 0x15, 0x1c, 0xf4, 0xde, 0x1e, 0x3b, 0x7c, 0x32, 0x1f, 0xf6, 0x47, 0xde, 0x6c, 0x7b, 0xec, - 0x8d, 0xbd, 0x6d, 0xa4, 0x0e, 0xe7, 0x4f, 0x71, 0x84, 0x03, 0xfc, 0x92, 0x52, 0xbd, 0xcd, 0xb1, - 0xe7, 0x8d, 0xa7, 0x2c, 0xe1, 0xe2, 0xce, 0x8c, 0x85, 0x9c, 0xce, 0x7c, 0xc5, 0xb0, 0x9b, 0x5a, - 0x8f, 0x33, 0xd7, 0x66, 0xc1, 0xcc, 0x71, 0x79, 0xfa, 0x73, 0xea, 0x0c, 0xc3, 0xed, 0x91, 0x37, - 0x9b, 0x79, 0x6e, 0xfa, 0x40, 0xbd, 0xbb, 0x5f, 0x29, 0x39, 0x0a, 0x4e, 0x7c, 0xee, 0x6d, 0xcf, - 0x58, 0xf0, 0x6c, 0xca, 0xd4, 0x3f, 0x29, 0x6c, 0xfc, 0xb6, 0x0c, 0x35, 0x93, 0x7d, 0x3a, 0x67, - 0x21, 0x27, 0x37, 0xa0, 0xcc, 0x46, 0x13, 0xaf, 0xbb, 0x7a, 0x55, 0xbb, 0xd1, 0xd8, 0x21, 0x7d, - 0xb9, 0x89, 0xa2, 0x3e, 0x18, 0x4d, 0xbc, 0xfd, 0x15, 0x13, 0x39, 0xc8, 0x4d, 0xa8, 0x3c, 0x9d, - 0xce, 0xc3, 0x49, 0xb7, 0x84, 0xac, 0xe7, 0xb3, 0xac, 0x3f, 0x12, 0xa4, 0xfd, 0x15, 0x53, 0xf2, - 0x88, 0x65, 0x1d, 0xf7, 0xa9, 0xd7, 0x2d, 0x17, 0x2d, 0x7b, 0xe0, 0x3e, 0xc5, 0x65, 0x05, 0x07, - 0xd9, 0x05, 0x08, 0x19, 0xb7, 0x3c, 0x9f, 0x3b, 0x9e, 0xdb, 0xad, 0x20, 0xff, 0xa5, 0x2c, 0xff, - 0x23, 0xc6, 0x7f, 0x82, 0xe4, 0xfd, 0x15, 0x53, 0x0f, 0xa3, 0x81, 0x90, 0x74, 0x5c, 0x87, 0x5b, - 0xa3, 0x09, 0x75, 0xdc, 0x6e, 0xb5, 0x48, 0xf2, 0xc0, 0x75, 0xf8, 0x3d, 0x41, 0x16, 0x92, 0x4e, - 0x34, 0x10, 0x57, 0xf9, 0x74, 0xce, 0x82, 0x93, 0x6e, 0xad, 0xe8, 0x2a, 0x3f, 0x15, 0x24, 0x71, - 0x15, 0xe4, 0x21, 0x77, 0xa1, 0x31, 0x64, 0x63, 0xc7, 0xb5, 0x86, 0x53, 0x6f, 0xf4, 0xac, 0x5b, - 0x47, 0x91, 0x6e, 0x56, 0x64, 0x20, 0x18, 0x06, 0x82, 0xbe, 0xbf, 0x62, 0xc2, 0x30, 0x1e, 0x91, - 0x1d, 0xa8, 0x8f, 0x26, 0x6c, 0xf4, 0xcc, 0xe2, 0x8b, 0xae, 0x8e, 0x92, 0x17, 0xb2, 0x92, 0xf7, - 0x04, 0xf5, 0xf1, 0x62, 0x7f, 0xc5, 0xac, 0x8d, 0xe4, 0x27, 0xb9, 0x0d, 0x3a, 0x73, 0x6d, 0xb5, - 0x5d, 0x03, 0x85, 0x2e, 0xe6, 0xde, 0xc5, 0xb5, 0xa3, 0xcd, 0xea, 0x4c, 0x7d, 0x93, 0x3e, 0x54, - 0x85, 0xa1, 0x38, 0xbc, 0xbb, 0x86, 0x32, 0xeb, 0xb9, 0x8d, 0x90, 0xb6, 0xbf, 0x62, 0x2a, 0x2e, - 0xa1, 0x3e, 0x9b, 0x4d, 0x9d, 0x63, 0x16, 0x88, 0xc3, 0x9d, 0x2f, 0x52, 0xdf, 0x7d, 0x49, 0xc7, - 0xe3, 0xe9, 0x76, 0x34, 0x18, 0xd4, 0xa0, 0x72, 0x4c, 0xa7, 0x73, 0x66, 0xbc, 0x09, 0x8d, 0x94, - 0xa5, 0x90, 0x2e, 0xd4, 0x66, 0x2c, 0x0c, 0xe9, 0x98, 0x75, 0xb5, 0xab, 0xda, 0x0d, 0xdd, 0x8c, - 0x86, 0x46, 0x0b, 0xd6, 0xd2, 0x76, 0x62, 0xcc, 0x62, 0x41, 0x61, 0x0b, 0x42, 0xf0, 0x98, 0x05, - 0xa1, 0x30, 0x00, 0x25, 0xa8, 0x86, 0xe4, 0x1a, 0x34, 0x51, 0x0f, 0x56, 0x44, 0x17, 0x76, 0x5a, - 0x36, 0xd7, 0x70, 0xf2, 0x48, 0x31, 0x6d, 0x42, 0xc3, 0xdf, 0xf1, 0x63, 0x96, 0x12, 0xb2, 0x80, - 0xbf, 0xe3, 0x2b, 0x06, 0xe3, 0x7d, 0xe8, 0xe4, 0x4d, 0x89, 0x74, 0xa0, 0xf4, 0x8c, 0x9d, 0xa8, - 0xfd, 0xc4, 0x27, 0x59, 0x57, 0xd7, 0xc2, 0x3d, 0x74, 0x53, 0xdd, 0xf1, 0xf3, 0xd5, 0x58, 0x38, - 0xb6, 0x26, 0xb2, 0x0b, 0x65, 0xe1, 0xcb, 0x28, 0xdd, 0xd8, 0xe9, 0xf5, 0xa5, 0xa3, 0xf7, 0x23, - 0x47, 0xef, 0x3f, 0x8e, 0x1c, 0x7d, 0x50, 0xff, 0xf2, 0xe5, 0xe6, 0xca, 0xe7, 0x7f, 0xd8, 0xd4, - 0x4c, 0x94, 0x20, 0x97, 0x85, 0x41, 0x50, 0xc7, 0xb5, 0x1c, 0x5b, 0xed, 0x53, 0xc3, 0xf1, 0x81, - 0x4d, 0xf6, 0xa0, 0x33, 0xf2, 0xdc, 0x90, 0xb9, 0xe1, 0x3c, 0xb4, 0x7c, 0x1a, 0xd0, 0x59, 0xa8, - 0x7c, 0x2d, 0x7a, 0xfe, 0x7b, 0x11, 0xf9, 0x10, 0xa9, 0x66, 0x7b, 0x94, 0x9d, 0x20, 0x1f, 0x00, - 0x1c, 0xd3, 0xa9, 0x63, 0x53, 0xee, 0x05, 0x61, 0xb7, 0x7c, 0xb5, 0x94, 0x12, 0x3e, 0x8a, 0x08, - 0x4f, 0x7c, 0x9b, 0x72, 0x36, 0x28, 0x8b, 0x93, 0x99, 0x29, 0x7e, 0x72, 0x1d, 0xda, 0xd4, 0xf7, - 0xad, 0x90, 0x53, 0xce, 0xac, 0xe1, 0x09, 0x67, 0x21, 0xfa, 0xe3, 0x9a, 0xd9, 0xa4, 0xbe, 0xff, - 0x48, 0xcc, 0x0e, 0xc4, 0xa4, 0x61, 0xc7, 0xaf, 0x89, 0xae, 0x42, 0x08, 0x94, 0x6d, 0xca, 0x29, - 0x6a, 0x63, 0xcd, 0xc4, 0x6f, 0x31, 0xe7, 0x53, 0x3e, 0x51, 0x77, 0xc4, 0x6f, 0x72, 0x11, 0xaa, - 0x13, 0xe6, 0x8c, 0x27, 0x1c, 0xaf, 0x55, 0x32, 0xd5, 0x48, 0x28, 0xde, 0x0f, 0xbc, 0x63, 0x86, - 0xd1, 0xa2, 0x6e, 0xca, 0x81, 0xf1, 0x17, 0x0d, 0xce, 0x9d, 0x71, 0x2f, 0xb1, 0xee, 0x84, 0x86, - 0x93, 0x68, 0x2f, 0xf1, 0x4d, 0x6e, 0x8a, 0x75, 0xa9, 0xcd, 0x02, 0x15, 0xc5, 0x9a, 0xea, 0xc6, - 0xfb, 0x38, 0xa9, 0x2e, 0xaa, 0x58, 0xc8, 0x03, 0xe8, 0x4c, 0x69, 0xc8, 0x2d, 0xe9, 0x05, 0x16, - 0x46, 0xa9, 0x52, 0xc6, 0x33, 0x1f, 0xd2, 0xc8, 0x5b, 0x84, 0x71, 0x2a, 0xf1, 0xd6, 0x34, 0x33, - 0x4b, 0xf6, 0x61, 0x7d, 0x78, 0xf2, 0x82, 0xba, 0xdc, 0x71, 0x99, 0x75, 0x46, 0xe7, 0x6d, 0xb5, - 0xd4, 0x83, 0x63, 0xc7, 0x66, 0xee, 0x28, 0x52, 0xf6, 0xf9, 0x58, 0x24, 0x7e, 0x8c, 0xd0, 0xd8, - 0x87, 0x56, 0x36, 0x16, 0x90, 0x16, 0xac, 0xf2, 0x85, 0xba, 0xe1, 0x2a, 0x5f, 0x90, 0xeb, 0x50, - 0x16, 0xcb, 0xe1, 0xed, 0x5a, 0x71, 0x30, 0x55, 0xdc, 0x8f, 0x4f, 0x7c, 0x66, 0x22, 0xdd, 0x30, - 0x62, 0x4b, 0x8d, 0x1d, 0x37, 0xbf, 0x96, 0xb1, 0x05, 0xed, 0x5c, 0x10, 0x49, 0x3d, 0x8b, 0x96, - 0x7e, 0x16, 0xa3, 0x0d, 0xcd, 0x4c, 0xec, 0x30, 0x3e, 0xab, 0x40, 0xdd, 0x64, 0xa1, 0x2f, 0x8c, - 0x8e, 0xec, 0x82, 0xce, 0x16, 0x23, 0x26, 0xc3, 0xb6, 0x96, 0x0b, 0x8a, 0x92, 0xe7, 0x41, 0x44, - 0x17, 0xe1, 0x23, 0x66, 0x26, 0x5b, 0x19, 0xc8, 0x39, 0x9f, 0x17, 0x4a, 0x63, 0xce, 0xad, 0x2c, - 0xe6, 0xac, 0xe7, 0x78, 0x73, 0xa0, 0xb3, 0x95, 0x01, 0x9d, 0xfc, 0xc2, 0x19, 0xd4, 0xb9, 0x53, - 0x80, 0x3a, 0xf9, 0xe3, 0x2f, 0x81, 0x9d, 0x3b, 0x05, 0xb0, 0xd3, 0x3d, 0xb3, 0x57, 0x21, 0xee, - 0xdc, 0xca, 0xe2, 0x4e, 0xfe, 0x3a, 0x39, 0xe0, 0xf9, 0xa0, 0x08, 0x78, 0x2e, 0xe7, 0x64, 0x96, - 0x22, 0xcf, 0x7b, 0x67, 0x90, 0xe7, 0x62, 0x4e, 0xb4, 0x00, 0x7a, 0xee, 0x64, 0x30, 0x01, 0x0a, - 0xef, 0x56, 0x0c, 0x0a, 0xe4, 0x07, 0x67, 0x51, 0xeb, 0x52, 0xfe, 0x69, 0x8b, 0x60, 0x6b, 0x3b, - 0x07, 0x5b, 0x17, 0xf2, 0xa7, 0xcc, 0xe1, 0x56, 0x82, 0x3e, 0x5b, 0x22, 0x3e, 0xe4, 0x2c, 0x4d, - 0xc4, 0x12, 0x16, 0x04, 0x5e, 0xa0, 0x02, 0xbb, 0x1c, 0x18, 0x37, 0x44, 0xc4, 0x4a, 0xec, 0xeb, - 0x15, 0x48, 0x85, 0x46, 0x9f, 0xb2, 0x2e, 0xe3, 0x0b, 0x2d, 0x91, 0x45, 0xcf, 0x4f, 0x47, 0x3b, - 0x5d, 0x45, 0xbb, 0x14, 0x80, 0xad, 0x66, 0x01, 0x6c, 0x13, 0x1a, 0x22, 0xa6, 0xe6, 0xb0, 0x89, - 0xfa, 0x11, 0x36, 0x91, 0xb7, 0xe0, 0x1c, 0xc6, 0x23, 0x09, 0x73, 0xca, 0x11, 0xcb, 0xe8, 0x88, - 0x6d, 0x41, 0x90, 0x1a, 0x93, 0x81, 0xf2, 0x6d, 0x38, 0x9f, 0xe2, 0x15, 0xeb, 0x62, 0x2c, 0x94, - 0x41, 0xba, 0x13, 0x73, 0xef, 0xf9, 0xfe, 0x3e, 0x0d, 0x27, 0xc6, 0x8f, 0x13, 0x05, 0x25, 0xb8, - 0x47, 0xa0, 0x3c, 0xf2, 0x6c, 0x79, 0xef, 0xa6, 0x89, 0xdf, 0x02, 0x0b, 0xa7, 0xde, 0x18, 0x0f, - 0xa7, 0x9b, 0xe2, 0x53, 0x70, 0xc5, 0xae, 0xa4, 0x4b, 0x9f, 0x31, 0x7e, 0xa9, 0x25, 0xeb, 0x25, - 0x50, 0x58, 0x84, 0x5a, 0xda, 0x7f, 0x83, 0x5a, 0xab, 0xdf, 0x0c, 0xb5, 0x8c, 0x53, 0x2d, 0x79, - 0xb2, 0x18, 0x8f, 0x5e, 0xef, 0x8a, 0xc2, 0x7a, 0x1c, 0xd7, 0x66, 0x0b, 0x54, 0x69, 0xc9, 0x94, - 0x83, 0x28, 0x55, 0xa8, 0xa2, 0x9a, 0xb3, 0xa9, 0x42, 0x0d, 0xe7, 0xe4, 0x80, 0x5c, 0x43, 0x1c, - 0xf3, 0x9e, 0x2a, 0x57, 0x6d, 0xf6, 0x55, 0xd6, 0x7d, 0x28, 0x26, 0x4d, 0x49, 0x4b, 0x45, 0x5b, - 0x3d, 0x03, 0x82, 0x57, 0x40, 0x17, 0x07, 0x0d, 0x7d, 0x3a, 0x62, 0xe8, 0x79, 0xba, 0x99, 0x4c, - 0x18, 0x8f, 0x81, 0x9c, 0xf5, 0x78, 0xf2, 0x21, 0x54, 0xd9, 0x31, 0x73, 0xb9, 0xd0, 0xb8, 0x50, - 0xda, 0x5a, 0x0c, 0x3b, 0xcc, 0xe5, 0x83, 0xae, 0x50, 0xd5, 0x5f, 0x5f, 0x6e, 0x76, 0x24, 0xcf, - 0x2d, 0x6f, 0xe6, 0x70, 0x36, 0xf3, 0xf9, 0x89, 0xa9, 0xa4, 0x8c, 0xbf, 0x6b, 0x02, 0x0d, 0x32, - 0xd1, 0xa0, 0x50, 0x79, 0x91, 0xc9, 0xaf, 0xa6, 0x00, 0xfe, 0xeb, 0x29, 0xf4, 0x3b, 0x00, 0x63, - 0x1a, 0x5a, 0xcf, 0xa9, 0xcb, 0x99, 0xad, 0xb4, 0xaa, 0x8f, 0x69, 0xf8, 0x33, 0x9c, 0x10, 0xd9, - 0x90, 0x20, 0xcf, 0x43, 0x66, 0xa3, 0x7a, 0x4b, 0x66, 0x6d, 0x4c, 0xc3, 0x27, 0x21, 0xb3, 0x53, - 0x77, 0xab, 0xbd, 0xce, 0xdd, 0xb2, 0xfa, 0xac, 0xe7, 0xf5, 0xf9, 0x2f, 0x0d, 0x2e, 0xe7, 0x6e, - 0x7e, 0x9f, 0xf9, 0x01, 0x1b, 0x51, 0x71, 0xac, 0xff, 0x17, 0x1d, 0xbc, 0x0f, 0x65, 0x4e, 0xc7, - 0x91, 0x06, 0x5a, 0x7d, 0x59, 0x08, 0xf6, 0x3f, 0x3e, 0x3a, 0xa4, 0x4e, 0x30, 0xb8, 0xa8, 0x74, - 0xd0, 0x12, 0x3c, 0x29, 0x0d, 0xa0, 0xcc, 0x57, 0xdc, 0xff, 0x9f, 0x29, 0x5f, 0x4e, 0x92, 0x85, - 0x6f, 0xc7, 0xdb, 0xff, 0x5b, 0x83, 0x37, 0xce, 0xdc, 0xfd, 0x5b, 0xf5, 0xfa, 0x7f, 0xd3, 0x44, - 0xa6, 0x98, 0x05, 0x65, 0x72, 0x00, 0xe7, 0xe2, 0xa8, 0x6a, 0xcd, 0x31, 0xda, 0x46, 0x71, 0xe5, - 0xd5, 0xc1, 0xb8, 0x73, 0x9c, 0x9d, 0x0e, 0xc9, 0x27, 0x70, 0x29, 0x87, 0x09, 0xf1, 0x82, 0xab, - 0xaf, 0x84, 0x86, 0x0b, 0x59, 0x68, 0x88, 0xd6, 0x4b, 0xec, 0xa1, 0xf4, 0x5a, 0x71, 0xee, 0xbb, - 0x22, 0xc5, 0x4e, 0xa7, 0x13, 0x45, 0xef, 0x69, 0xfc, 0x5a, 0x83, 0x76, 0xee, 0x40, 0xe4, 0x36, - 0x80, 0x04, 0xdb, 0xd0, 0x79, 0xc1, 0x72, 0xb8, 0x86, 0x6a, 0x7b, 0xe4, 0xbc, 0x60, 0xea, 0xf0, - 0xfa, 0x30, 0x9a, 0x20, 0xef, 0x42, 0x9d, 0xa9, 0xd4, 0x5f, 0xdd, 0xf8, 0x42, 0xae, 0x22, 0x50, - 0x32, 0x31, 0x1b, 0xf9, 0x3e, 0xe8, 0xb1, 0x1e, 0x73, 0x65, 0x5f, 0xac, 0xf6, 0x68, 0xa3, 0x98, - 0xd1, 0xf8, 0x08, 0xda, 0xb9, 0x63, 0x90, 0x37, 0x40, 0x9f, 0xd1, 0x85, 0xaa, 0xdf, 0x64, 0x46, - 0x5f, 0x9f, 0xd1, 0x05, 0x96, 0x6e, 0xe4, 0x12, 0xd4, 0x04, 0x71, 0x4c, 0xe5, 0x4b, 0x94, 0xcc, - 0xea, 0x8c, 0x2e, 0x3e, 0xa2, 0xa1, 0xb1, 0x05, 0xad, 0xec, 0xd1, 0x22, 0xd6, 0x28, 0x47, 0x92, - 0xac, 0x7b, 0x63, 0x66, 0xdc, 0x86, 0x76, 0xee, 0x44, 0xc4, 0x80, 0xa6, 0x3f, 0x1f, 0x5a, 0xcf, - 0xd8, 0x89, 0x85, 0x47, 0x46, 0xbb, 0xd1, 0xcd, 0x86, 0x3f, 0x1f, 0x7e, 0xcc, 0x4e, 0x44, 0x89, - 0x12, 0x1a, 0x8f, 0xa0, 0x95, 0xad, 0xac, 0x04, 0x8a, 0x06, 0xde, 0xdc, 0xb5, 0x71, 0xfd, 0x8a, - 0x29, 0x07, 0xe4, 0x26, 0x54, 0x8e, 0x3d, 0x69, 0x2a, 0xe9, 0x52, 0xea, 0xc8, 0xe3, 0x2c, 0x55, - 0x8f, 0x49, 0x1e, 0xc3, 0x81, 0x0a, 0x1a, 0x81, 0x78, 0x50, 0xac, 0x91, 0x54, 0x56, 0x26, 0xbe, - 0xc9, 0x43, 0x00, 0xca, 0x79, 0xe0, 0x0c, 0xe7, 0xc9, 0x72, 0x79, 0x37, 0xba, 0xa2, 0x8c, 0x67, - 0x3d, 0xe1, 0x4c, 0x19, 0x50, 0x4a, 0xde, 0xf8, 0x45, 0x05, 0xaa, 0xb2, 0xa2, 0x24, 0xfd, 0x6c, - 0xbf, 0x42, 0xac, 0xaa, 0x0e, 0x29, 0x67, 0xd5, 0x19, 0xe3, 0x24, 0xf0, 0x7a, 0xbe, 0xe8, 0x1f, - 0x34, 0x4e, 0x5f, 0x6e, 0xd6, 0x30, 0x81, 0x3a, 0xb8, 0x9f, 0x74, 0x00, 0x96, 0x15, 0xc8, 0x51, - 0xbb, 0xa1, 0xfc, 0x8d, 0xdb, 0x0d, 0x97, 0xa0, 0xe6, 0xce, 0x67, 0x16, 0x5f, 0x84, 0x2a, 0xf4, - 0x54, 0xdd, 0xf9, 0xec, 0xf1, 0x02, 0xad, 0x84, 0x7b, 0x9c, 0x4e, 0x91, 0x24, 0x03, 0x4f, 0x1d, - 0x27, 0x04, 0x71, 0x17, 0x9a, 0xa9, 0x3c, 0xd3, 0xb1, 0x55, 0xbd, 0xd2, 0x4a, 0x1b, 0xfe, 0xc1, - 0x7d, 0x75, 0xcb, 0x46, 0x9c, 0x77, 0x1e, 0xd8, 0xe4, 0x46, 0xb6, 0xba, 0xc6, 0xf4, 0xb4, 0x8e, - 0x3e, 0x96, 0x2a, 0xa0, 0x45, 0x72, 0x2a, 0x0e, 0x20, 0xbc, 0x4e, 0xb2, 0xe8, 0xc8, 0x52, 0x17, - 0x13, 0x48, 0x7c, 0x13, 0xda, 0x49, 0x86, 0x27, 0x59, 0x40, 0xae, 0x92, 0x4c, 0x23, 0xe3, 0x3b, - 0xb0, 0xee, 0xb2, 0x05, 0xb7, 0xf2, 0xdc, 0x0d, 0xe4, 0x26, 0x82, 0x76, 0x94, 0x95, 0xf8, 0x1e, - 0xb4, 0x92, 0xd8, 0x84, 0xbc, 0x6b, 0xb2, 0xc7, 0x11, 0xcf, 0x22, 0xdb, 0x65, 0xa8, 0xc7, 0xf9, - 0x75, 0x13, 0x19, 0x6a, 0x54, 0xa6, 0xd5, 0x71, 0xc6, 0x1e, 0xb0, 0x70, 0x3e, 0xe5, 0x6a, 0x91, - 0x16, 0xf2, 0x60, 0xc6, 0x6e, 0xca, 0x79, 0xe4, 0xbd, 0x06, 0xcd, 0xc8, 0xc3, 0x25, 0x5f, 0x1b, - 0xf9, 0xd6, 0xa2, 0x49, 0x64, 0xda, 0x82, 0x8e, 0x1f, 0x78, 0xbe, 0x17, 0xb2, 0xc0, 0xa2, 0xb6, - 0x1d, 0xb0, 0x30, 0xec, 0x76, 0xe4, 0x7a, 0xd1, 0xfc, 0x9e, 0x9c, 0x36, 0xde, 0x85, 0x5a, 0x54, - 0x38, 0xac, 0x43, 0x05, 0xb5, 0x8e, 0x26, 0x58, 0x36, 0xe5, 0x40, 0x80, 0xd2, 0x9e, 0xef, 0xab, - 0x36, 0x99, 0xf8, 0x34, 0x7e, 0x0e, 0x35, 0xf5, 0x60, 0x85, 0xcd, 0x93, 0x1f, 0xc2, 0x9a, 0x4f, - 0x03, 0x71, 0x8d, 0x74, 0x0b, 0x25, 0x2a, 0x4d, 0x0f, 0x69, 0xc0, 0x1f, 0x31, 0x9e, 0xe9, 0xa4, - 0x34, 0x90, 0x5f, 0x4e, 0x19, 0x77, 0xa0, 0x99, 0xe1, 0x11, 0xc7, 0x42, 0x3b, 0x8a, 0x9c, 0x1a, - 0x07, 0xf1, 0xce, 0xab, 0xc9, 0xce, 0xc6, 0x5d, 0xd0, 0xe3, 0xb7, 0x11, 0x15, 0x54, 0x74, 0x75, - 0x4d, 0xa9, 0x5b, 0x0e, 0xb1, 0x3b, 0xe4, 0x3d, 0x67, 0x81, 0xf2, 0x09, 0x39, 0x30, 0x9e, 0xa4, - 0x82, 0x90, 0x84, 0x09, 0x72, 0x0b, 0x6a, 0x2a, 0x08, 0x29, 0xaf, 0x8c, 0xfa, 0x40, 0x87, 0x18, - 0x85, 0xa2, 0x3e, 0x90, 0x8c, 0x49, 0xc9, 0xb2, 0xab, 0xe9, 0x65, 0xa7, 0x50, 0x8f, 0x02, 0x4d, - 0x36, 0x22, 0xcb, 0x15, 0x3b, 0xf9, 0x88, 0xac, 0x16, 0x4d, 0x18, 0x85, 0x75, 0x84, 0xce, 0xd8, - 0x65, 0xb6, 0x95, 0xb8, 0x10, 0xee, 0x51, 0x37, 0xdb, 0x92, 0xf0, 0x30, 0xf2, 0x17, 0xe3, 0x1d, - 0xa8, 0xca, 0xb3, 0x15, 0x86, 0xaf, 0x22, 0x8c, 0xfa, 0xbd, 0x06, 0xf5, 0x28, 0x4e, 0x17, 0x0a, - 0x65, 0x0e, 0xbd, 0xfa, 0x75, 0x0f, 0xfd, 0xbf, 0x0f, 0x3c, 0xb7, 0x80, 0xc8, 0xf8, 0x72, 0xec, - 0x71, 0xc7, 0x1d, 0x5b, 0x52, 0xd7, 0x32, 0x06, 0x75, 0x90, 0x72, 0x84, 0x84, 0x43, 0x31, 0xff, - 0xd6, 0x35, 0x68, 0xa4, 0xda, 0x59, 0xa4, 0x06, 0xa5, 0x4f, 0xd8, 0xf3, 0xce, 0x0a, 0x69, 0x40, - 0xcd, 0x64, 0xd8, 0x9c, 0xe8, 0x68, 0x3b, 0x9f, 0x55, 0xa0, 0xbd, 0x37, 0xb8, 0x77, 0xb0, 0xe7, - 0xfb, 0x53, 0x67, 0x44, 0xb1, 0x9a, 0xdd, 0x86, 0x32, 0x16, 0xf4, 0x05, 0x3f, 0x5c, 0xf4, 0x8a, - 0x3a, 0x4b, 0x64, 0x07, 0x2a, 0x58, 0xd7, 0x93, 0xa2, 0xdf, 0x2f, 0x7a, 0x85, 0x0d, 0x26, 0xb1, - 0x89, 0xac, 0xfc, 0xcf, 0xfe, 0x8c, 0xd1, 0x2b, 0xea, 0x32, 0x91, 0x0f, 0x41, 0x4f, 0x0a, 0xee, - 0x65, 0x3f, 0x66, 0xf4, 0x96, 0xf6, 0x9b, 0x84, 0x7c, 0x92, 0x94, 0x2f, 0xeb, 0xc9, 0xf7, 0x96, - 0x36, 0x66, 0xc8, 0x2e, 0xd4, 0xa2, 0x72, 0xae, 0xf8, 0xe7, 0x86, 0xde, 0x92, 0x5e, 0x90, 0x50, - 0x8f, 0xac, 0xa1, 0x8b, 0x7e, 0x13, 0xe9, 0x15, 0x36, 0xac, 0xc8, 0x6d, 0xa8, 0xaa, 0xac, 0xaa, - 0xf0, 0x27, 0x87, 0x5e, 0x71, 0x47, 0x47, 0x5c, 0x32, 0xe9, 0x22, 0x2c, 0xfb, 0xdd, 0xa6, 0xb7, - 0xb4, 0xb3, 0x46, 0xf6, 0x00, 0x52, 0xa5, 0xf0, 0xd2, 0x1f, 0x64, 0x7a, 0xcb, 0x3b, 0x66, 0xe4, - 0x2e, 0xd4, 0x93, 0x2e, 0x68, 0xf1, 0x4f, 0x2c, 0xbd, 0x65, 0x4d, 0xac, 0xc1, 0x95, 0x7f, 0xfc, - 0x69, 0x43, 0xfb, 0xd5, 0xe9, 0x86, 0xf6, 0xc5, 0xe9, 0x86, 0xf6, 0xe5, 0xe9, 0x86, 0xf6, 0xbb, - 0xd3, 0x0d, 0xed, 0x8f, 0xa7, 0x1b, 0xda, 0x6f, 0xfe, 0xbc, 0xa1, 0x0d, 0xab, 0xe8, 0x23, 0xef, - 0xfd, 0x27, 0x00, 0x00, 0xff, 0xff, 0xad, 0x6a, 0xd3, 0xce, 0x52, 0x1c, 0x00, 0x00, + golang_proto.RegisterFile("abci/types/types.proto", fileDescriptor_types_765aa6431c012acb) +} + +var fileDescriptor_types_765aa6431c012acb = []byte{ + // 2388 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe4, 0x59, 0xcb, 0x6f, 0x1c, 0xc7, + 0xd1, 0xe7, 0xec, 0x7b, 0x6b, 0xb9, 0x0f, 0xb5, 0x28, 0x69, 0xb5, 0xd6, 0x47, 0x0a, 0xa3, 0x2f, + 0xb2, 0x68, 0xc9, 0x4b, 0x9b, 0x8e, 0x02, 0xca, 0x72, 0x0c, 0x70, 0x25, 0xc5, 0x24, 0xac, 0x38, + 0xcc, 0x48, 0x62, 0x80, 0x20, 0xc0, 0xa0, 0x77, 0xa7, 0xb5, 0x3b, 0xe0, 0xee, 0xcc, 0x78, 0xa6, + 0x97, 0x5a, 0xea, 0x98, 0xb3, 0x0f, 0x3e, 0xe4, 0x8f, 0xc8, 0x35, 0x37, 0x1f, 0x73, 0x0a, 0x7c, + 0xcc, 0x21, 0x67, 0x25, 0x61, 0x90, 0x4b, 0x80, 0x9c, 0x72, 0x48, 0x72, 0x08, 0x12, 0x74, 0x75, + 0xcf, 0x93, 0xb3, 0xb2, 0xa5, 0xe4, 0x60, 0xc0, 0x17, 0x72, 0xba, 0xeb, 0x57, 0xd5, 0x5d, 0xd5, + 0xf5, 0xe8, 0xae, 0x85, 0x8b, 0x74, 0x38, 0xb2, 0xb7, 0xf8, 0x89, 0xc7, 0x02, 0xf9, 0xb7, 0xef, + 0xf9, 0x2e, 0x77, 0x49, 0x19, 0x07, 0xbd, 0xb7, 0xc7, 0x36, 0x9f, 0xcc, 0x87, 0xfd, 0x91, 0x3b, + 0xdb, 0x1a, 0xbb, 0x63, 0x77, 0x0b, 0xa9, 0xc3, 0xf9, 0x53, 0x1c, 0xe1, 0x00, 0xbf, 0x24, 0x57, + 0x6f, 0x63, 0xec, 0xba, 0xe3, 0x29, 0x8b, 0x51, 0xdc, 0x9e, 0xb1, 0x80, 0xd3, 0x99, 0xa7, 0x00, + 0x3b, 0x09, 0x79, 0x9c, 0x39, 0x16, 0xf3, 0x67, 0xb6, 0xc3, 0x93, 0x9f, 0x53, 0x7b, 0x18, 0x6c, + 0x8d, 0xdc, 0xd9, 0xcc, 0x75, 0x92, 0x1b, 0xea, 0xdd, 0xfd, 0x4a, 0xce, 0x91, 0x7f, 0xe2, 0x71, + 0x77, 0x6b, 0xc6, 0xfc, 0xa3, 0x29, 0x53, 0xff, 0x24, 0xb3, 0xfe, 0x9b, 0x12, 0x54, 0x0d, 0xf6, + 0xe9, 0x9c, 0x05, 0x9c, 0xdc, 0x80, 0x12, 0x1b, 0x4d, 0xdc, 0x6e, 0xe1, 0xaa, 0x76, 0xa3, 0xb1, + 0x4d, 0xfa, 0x72, 0x11, 0x45, 0x7d, 0x30, 0x9a, 0xb8, 0x7b, 0x2b, 0x06, 0x22, 0xc8, 0x4d, 0x28, + 0x3f, 0x9d, 0xce, 0x83, 0x49, 0xb7, 0x88, 0xd0, 0xf3, 0x69, 0xe8, 0x0f, 0x04, 0x69, 0x6f, 0xc5, + 0x90, 0x18, 0x21, 0xd6, 0x76, 0x9e, 0xba, 0xdd, 0x52, 0x9e, 0xd8, 0x7d, 0xe7, 0x29, 0x8a, 0x15, + 0x08, 0xb2, 0x03, 0x10, 0x30, 0x6e, 0xba, 0x1e, 0xb7, 0x5d, 0xa7, 0x5b, 0x46, 0xfc, 0xa5, 0x34, + 0xfe, 0x11, 0xe3, 0x3f, 0x42, 0xf2, 0xde, 0x8a, 0x51, 0x0f, 0xc2, 0x81, 0xe0, 0xb4, 0x1d, 0x9b, + 0x9b, 0xa3, 0x09, 0xb5, 0x9d, 0x6e, 0x25, 0x8f, 0x73, 0xdf, 0xb1, 0xf9, 0x3d, 0x41, 0x16, 0x9c, + 0x76, 0x38, 0x10, 0xaa, 0x7c, 0x3a, 0x67, 0xfe, 0x49, 0xb7, 0x9a, 0xa7, 0xca, 0x8f, 0x05, 0x49, + 0xa8, 0x82, 0x18, 0x72, 0x17, 0x1a, 0x43, 0x36, 0xb6, 0x1d, 0x73, 0x38, 0x75, 0x47, 0x47, 0xdd, + 0x1a, 0xb2, 0x74, 0xd3, 0x2c, 0x03, 0x01, 0x18, 0x08, 0xfa, 0xde, 0x8a, 0x01, 0xc3, 0x68, 0x44, + 0xb6, 0xa1, 0x36, 0x9a, 0xb0, 0xd1, 0x91, 0xc9, 0x17, 0xdd, 0x3a, 0x72, 0x5e, 0x48, 0x73, 0xde, + 0x13, 0xd4, 0xc7, 0x8b, 0xbd, 0x15, 0xa3, 0x3a, 0x92, 0x9f, 0xe4, 0x36, 0xd4, 0x99, 0x63, 0xa9, + 0xe5, 0x1a, 0xc8, 0x74, 0x31, 0x73, 0x2e, 0x8e, 0x15, 0x2e, 0x56, 0x63, 0xea, 0x9b, 0xf4, 0xa1, + 0x22, 0x1c, 0xc5, 0xe6, 0xdd, 0x55, 0xe4, 0x59, 0xcb, 0x2c, 0x84, 0xb4, 0xbd, 0x15, 0x43, 0xa1, + 0x84, 0xf9, 0x2c, 0x36, 0xb5, 0x8f, 0x99, 0x2f, 0x36, 0x77, 0x3e, 0xcf, 0x7c, 0xf7, 0x25, 0x1d, + 0xb7, 0x57, 0xb7, 0xc2, 0xc1, 0xa0, 0x0a, 0xe5, 0x63, 0x3a, 0x9d, 0x33, 0xfd, 0x4d, 0x68, 0x24, + 0x3c, 0x85, 0x74, 0xa1, 0x3a, 0x63, 0x41, 0x40, 0xc7, 0xac, 0xab, 0x5d, 0xd5, 0x6e, 0xd4, 0x8d, + 0x70, 0xa8, 0xb7, 0x60, 0x35, 0xe9, 0x27, 0xfa, 0x2c, 0x62, 0x14, 0xbe, 0x20, 0x18, 0x8f, 0x99, + 0x1f, 0x08, 0x07, 0x50, 0x8c, 0x6a, 0x48, 0xae, 0x41, 0x13, 0xed, 0x60, 0x86, 0x74, 0xe1, 0xa7, + 0x25, 0x63, 0x15, 0x27, 0x0f, 0x15, 0x68, 0x03, 0x1a, 0xde, 0xb6, 0x17, 0x41, 0x8a, 0x08, 0x01, + 0x6f, 0xdb, 0x53, 0x00, 0xfd, 0x7d, 0xe8, 0x64, 0x5d, 0x89, 0x74, 0xa0, 0x78, 0xc4, 0x4e, 0xd4, + 0x7a, 0xe2, 0x93, 0xac, 0x29, 0xb5, 0x70, 0x8d, 0xba, 0xa1, 0x74, 0xfc, 0xbc, 0x10, 0x31, 0x47, + 0xde, 0x44, 0x76, 0xa0, 0x24, 0x62, 0x19, 0xb9, 0x1b, 0xdb, 0xbd, 0xbe, 0x0c, 0xf4, 0x7e, 0x18, + 0xe8, 0xfd, 0xc7, 0x61, 0xa0, 0x0f, 0x6a, 0x5f, 0xbe, 0xd8, 0x58, 0xf9, 0xfc, 0xf7, 0x1b, 0x9a, + 0x81, 0x1c, 0xe4, 0xb2, 0x70, 0x08, 0x6a, 0x3b, 0xa6, 0x6d, 0xa9, 0x75, 0xaa, 0x38, 0xde, 0xb7, + 0xc8, 0x2e, 0x74, 0x46, 0xae, 0x13, 0x30, 0x27, 0x98, 0x07, 0xa6, 0x47, 0x7d, 0x3a, 0x0b, 0x54, + 0xac, 0x85, 0xc7, 0x7f, 0x2f, 0x24, 0x1f, 0x20, 0xd5, 0x68, 0x8f, 0xd2, 0x13, 0xe4, 0x03, 0x80, + 0x63, 0x3a, 0xb5, 0x2d, 0xca, 0x5d, 0x3f, 0xe8, 0x96, 0xae, 0x16, 0x13, 0xcc, 0x87, 0x21, 0xe1, + 0x89, 0x67, 0x51, 0xce, 0x06, 0x25, 0xb1, 0x33, 0x23, 0x81, 0x27, 0xd7, 0xa1, 0x4d, 0x3d, 0xcf, + 0x0c, 0x38, 0xe5, 0xcc, 0x1c, 0x9e, 0x70, 0x16, 0x60, 0x3c, 0xae, 0x1a, 0x4d, 0xea, 0x79, 0x8f, + 0xc4, 0xec, 0x40, 0x4c, 0xea, 0x56, 0x74, 0x9a, 0x18, 0x2a, 0x84, 0x40, 0xc9, 0xa2, 0x9c, 0xa2, + 0x35, 0x56, 0x0d, 0xfc, 0x16, 0x73, 0x1e, 0xe5, 0x13, 0xa5, 0x23, 0x7e, 0x93, 0x8b, 0x50, 0x99, + 0x30, 0x7b, 0x3c, 0xe1, 0xa8, 0x56, 0xd1, 0x50, 0x23, 0x61, 0x78, 0xcf, 0x77, 0x8f, 0x19, 0x66, + 0x8b, 0x9a, 0x21, 0x07, 0xfa, 0x9f, 0x35, 0x38, 0x77, 0x26, 0xbc, 0x84, 0xdc, 0x09, 0x0d, 0x26, + 0xe1, 0x5a, 0xe2, 0x9b, 0xdc, 0x14, 0x72, 0xa9, 0xc5, 0x7c, 0x95, 0xc5, 0x9a, 0x4a, 0xe3, 0x3d, + 0x9c, 0x54, 0x8a, 0x2a, 0x08, 0x79, 0x00, 0x9d, 0x29, 0x0d, 0xb8, 0x29, 0xa3, 0xc0, 0xc4, 0x2c, + 0x55, 0x4c, 0x45, 0xe6, 0x43, 0x1a, 0x46, 0x8b, 0x70, 0x4e, 0xc5, 0xde, 0x9a, 0xa6, 0x66, 0xc9, + 0x1e, 0xac, 0x0d, 0x4f, 0x9e, 0x53, 0x87, 0xdb, 0x0e, 0x33, 0xcf, 0xd8, 0xbc, 0xad, 0x44, 0x3d, + 0x38, 0xb6, 0x2d, 0xe6, 0x8c, 0x42, 0x63, 0x9f, 0x8f, 0x58, 0xa2, 0xc3, 0x08, 0xf4, 0x3d, 0x68, + 0xa5, 0x73, 0x01, 0x69, 0x41, 0x81, 0x2f, 0x94, 0x86, 0x05, 0xbe, 0x20, 0xd7, 0xa1, 0x24, 0xc4, + 0xa1, 0x76, 0xad, 0x28, 0x99, 0x2a, 0xf4, 0xe3, 0x13, 0x8f, 0x19, 0x48, 0xd7, 0xf5, 0xc8, 0x53, + 0xa3, 0xc0, 0xcd, 0xca, 0xd2, 0x37, 0xa1, 0x9d, 0x49, 0x22, 0x89, 0x63, 0xd1, 0x92, 0xc7, 0xa2, + 0xb7, 0xa1, 0x99, 0xca, 0x1d, 0xfa, 0x67, 0x65, 0xa8, 0x19, 0x2c, 0xf0, 0x84, 0xd3, 0x91, 0x1d, + 0xa8, 0xb3, 0xc5, 0x88, 0xc9, 0xb4, 0xad, 0x65, 0x92, 0xa2, 0xc4, 0x3c, 0x08, 0xe9, 0x22, 0x7d, + 0x44, 0x60, 0xb2, 0x99, 0x2a, 0x39, 0xe7, 0xb3, 0x4c, 0xc9, 0x9a, 0x73, 0x2b, 0x5d, 0x73, 0xd6, + 0x32, 0xd8, 0x4c, 0xd1, 0xd9, 0x4c, 0x15, 0x9d, 0xac, 0xe0, 0x54, 0xd5, 0xb9, 0x93, 0x53, 0x75, + 0xb2, 0xdb, 0x5f, 0x52, 0x76, 0xee, 0xe4, 0x94, 0x9d, 0xee, 0x99, 0xb5, 0x72, 0xeb, 0xce, 0xad, + 0x74, 0xdd, 0xc9, 0xaa, 0x93, 0x29, 0x3c, 0x1f, 0xe4, 0x15, 0x9e, 0xcb, 0x19, 0x9e, 0xa5, 0x95, + 0xe7, 0xbd, 0x33, 0x95, 0xe7, 0x62, 0x86, 0x35, 0xa7, 0xf4, 0xdc, 0x49, 0xd5, 0x04, 0xc8, 0xd5, + 0x2d, 0xbf, 0x28, 0x90, 0xef, 0x9d, 0xad, 0x5a, 0x97, 0xb2, 0x47, 0x9b, 0x57, 0xb6, 0xb6, 0x32, + 0x65, 0xeb, 0x42, 0x76, 0x97, 0x99, 0xba, 0x15, 0x57, 0x9f, 0x4d, 0x91, 0x1f, 0x32, 0x9e, 0x26, + 0x72, 0x09, 0xf3, 0x7d, 0xd7, 0x57, 0x89, 0x5d, 0x0e, 0xf4, 0x1b, 0x22, 0x63, 0xc5, 0xfe, 0xf5, + 0x92, 0x4a, 0x85, 0x4e, 0x9f, 0xf0, 0x2e, 0xfd, 0x0b, 0x2d, 0xe6, 0xc5, 0xc8, 0x4f, 0x66, 0xbb, + 0xba, 0xca, 0x76, 0x89, 0x02, 0x56, 0x48, 0x17, 0xb0, 0x0d, 0x68, 0x88, 0x9c, 0x9a, 0xa9, 0x4d, + 0xd4, 0x0b, 0x6b, 0x13, 0x79, 0x0b, 0xce, 0x61, 0x3e, 0x92, 0x65, 0x4e, 0x05, 0x62, 0x09, 0x03, + 0xb1, 0x2d, 0x08, 0xd2, 0x62, 0x32, 0x51, 0xbe, 0x0d, 0xe7, 0x13, 0x58, 0x21, 0x17, 0x73, 0xa1, + 0x4c, 0xd2, 0x9d, 0x08, 0xbd, 0xeb, 0x79, 0x7b, 0x34, 0x98, 0xe8, 0x3f, 0x8c, 0x0d, 0x14, 0xd7, + 0x3d, 0x02, 0xa5, 0x91, 0x6b, 0x49, 0xbd, 0x9b, 0x06, 0x7e, 0x8b, 0x5a, 0x38, 0x75, 0xc7, 0xb8, + 0xb9, 0xba, 0x21, 0x3e, 0x05, 0x2a, 0x0a, 0xa5, 0xba, 0x8c, 0x19, 0xfd, 0x17, 0x5a, 0x2c, 0x2f, + 0x2e, 0x85, 0x79, 0x55, 0x4b, 0xfb, 0x6f, 0xaa, 0x56, 0xe1, 0xd5, 0xaa, 0x96, 0x7e, 0xaa, 0xc5, + 0x47, 0x16, 0xd5, 0xa3, 0xd7, 0x53, 0x51, 0x78, 0x8f, 0xed, 0x58, 0x6c, 0x81, 0x26, 0x2d, 0x1a, + 0x72, 0x10, 0x5e, 0x15, 0x2a, 0x68, 0xe6, 0xf4, 0x55, 0xa1, 0x8a, 0x73, 0x72, 0x40, 0xae, 0x61, + 0x1d, 0x73, 0x9f, 0xaa, 0x50, 0x6d, 0xf6, 0xd5, 0xad, 0xfb, 0x40, 0x4c, 0x1a, 0x92, 0x96, 0xc8, + 0xb6, 0xf5, 0x54, 0x11, 0xbc, 0x02, 0x75, 0xb1, 0xd1, 0xc0, 0xa3, 0x23, 0x86, 0x91, 0x57, 0x37, + 0xe2, 0x09, 0xfd, 0x31, 0x90, 0xb3, 0x11, 0x4f, 0x3e, 0x84, 0x0a, 0x3b, 0x66, 0x0e, 0x17, 0x16, + 0x17, 0x46, 0x5b, 0x8d, 0xca, 0x0e, 0x73, 0xf8, 0xa0, 0x2b, 0x4c, 0xf5, 0x97, 0x17, 0x1b, 0x1d, + 0x89, 0xb9, 0xe5, 0xce, 0x6c, 0xce, 0x66, 0x1e, 0x3f, 0x31, 0x14, 0x97, 0xfe, 0x53, 0xb8, 0x72, + 0x56, 0xea, 0x7d, 0xe6, 0xf9, 0x6c, 0x44, 0x39, 0xb3, 0xc8, 0xfb, 0x50, 0xe2, 0x74, 0x1c, 0x4a, + 0x6f, 0xf5, 0xe5, 0x43, 0xa4, 0xff, 0xf1, 0xe1, 0x01, 0xb5, 0xfd, 0xc1, 0x45, 0x25, 0xbf, 0x25, + 0x30, 0x09, 0xe9, 0xc8, 0xa3, 0xff, 0x5d, 0x13, 0x95, 0x26, 0x95, 0x69, 0x72, 0x0f, 0x26, 0x0c, + 0xa7, 0x42, 0xe2, 0xf2, 0xf0, 0xf5, 0x0e, 0xeb, 0xff, 0x00, 0xc6, 0x34, 0x30, 0x9f, 0x51, 0x87, + 0x33, 0x4b, 0x9d, 0x58, 0x7d, 0x4c, 0x83, 0x9f, 0xe0, 0x84, 0xb8, 0x69, 0x09, 0xf2, 0x3c, 0x60, + 0x16, 0x1e, 0x5d, 0xd1, 0xa8, 0x8e, 0x69, 0xf0, 0x24, 0x60, 0x56, 0xc2, 0x6e, 0xd5, 0xd7, 0xb1, + 0x5b, 0xfa, 0xac, 0x6a, 0xd9, 0xb3, 0xfa, 0x97, 0x06, 0x97, 0x33, 0x9a, 0x27, 0x6c, 0xfa, 0x4d, + 0xb1, 0x41, 0x78, 0xb6, 0xd5, 0x57, 0x3f, 0xdb, 0xaf, 0xd0, 0xff, 0x9f, 0x89, 0x3c, 0x11, 0x5f, + 0x44, 0xbe, 0x1d, 0x67, 0xff, 0x6f, 0x0d, 0xde, 0x38, 0xa3, 0xfb, 0xb7, 0xea, 0xf4, 0xff, 0xaa, + 0x89, 0x5b, 0x68, 0xba, 0xe0, 0x93, 0x7d, 0x38, 0x17, 0x65, 0x6c, 0x73, 0x8e, 0x99, 0x3c, 0xcc, + 0x2a, 0x2f, 0x4f, 0xf4, 0x9d, 0xe3, 0xf4, 0x74, 0x40, 0x3e, 0x81, 0x4b, 0x99, 0x7a, 0x13, 0x09, + 0x2c, 0xbc, 0xb4, 0xec, 0x5c, 0x48, 0x97, 0x9d, 0x50, 0x5e, 0xec, 0x0f, 0xc5, 0xd7, 0xca, 0xa1, + 0x7f, 0xd3, 0xa0, 0x97, 0xd5, 0x37, 0x71, 0xe0, 0xdf, 0x60, 0xcd, 0x43, 0x1f, 0x28, 0xbe, 0x46, + 0x76, 0xff, 0x7f, 0xf1, 0x68, 0x49, 0x5e, 0xd0, 0xf2, 0xbc, 0x58, 0xff, 0x95, 0x06, 0xed, 0xcc, + 0x66, 0xc8, 0x6d, 0x00, 0x79, 0x7d, 0x09, 0xec, 0xe7, 0x2c, 0x73, 0x53, 0x40, 0xe3, 0x3d, 0xb2, + 0x9f, 0x33, 0xb5, 0xf1, 0xfa, 0x30, 0x9c, 0x20, 0xef, 0x42, 0x8d, 0xa9, 0xc7, 0x94, 0xd2, 0xf6, + 0x42, 0xe6, 0x8d, 0xa5, 0x78, 0x22, 0x18, 0xf9, 0x2e, 0xd4, 0x23, 0x1b, 0x66, 0x1e, 0xd2, 0x91, + 0xc9, 0xc3, 0x85, 0x22, 0xa0, 0xfe, 0x11, 0xb4, 0x33, 0xdb, 0x20, 0x6f, 0x40, 0x7d, 0x46, 0x17, + 0xea, 0x45, 0x2c, 0xdf, 0x48, 0xb5, 0x19, 0x5d, 0xe0, 0x63, 0x98, 0x5c, 0x82, 0xaa, 0x20, 0x8e, + 0xa9, 0x3c, 0x85, 0xa2, 0x51, 0x99, 0xd1, 0xc5, 0x47, 0x34, 0xd0, 0x37, 0xa1, 0x95, 0xde, 0x5a, + 0x08, 0x0d, 0x6f, 0x9d, 0x12, 0xba, 0x3b, 0x66, 0xfa, 0x6d, 0x68, 0x67, 0x76, 0x44, 0x74, 0x68, + 0x7a, 0xf3, 0xa1, 0x79, 0xc4, 0x4e, 0x4c, 0xdc, 0x32, 0xfa, 0x4c, 0xdd, 0x68, 0x78, 0xf3, 0xe1, + 0xc7, 0xec, 0x44, 0x3c, 0xfa, 0x02, 0xfd, 0x11, 0xb4, 0xd2, 0x6f, 0x55, 0x71, 0x2f, 0xf1, 0xdd, + 0xb9, 0x63, 0xa1, 0xfc, 0xb2, 0x21, 0x07, 0xe4, 0x26, 0x94, 0x8f, 0x5d, 0xe9, 0x26, 0xc9, 0xc7, + 0xe9, 0xa1, 0xcb, 0x59, 0xe2, 0x85, 0x2b, 0x31, 0xba, 0x0d, 0x65, 0x74, 0x7d, 0x71, 0xa0, 0xf8, + 0xea, 0x54, 0xf7, 0x5c, 0xf1, 0x4d, 0x1e, 0x02, 0x50, 0xce, 0x7d, 0x7b, 0x38, 0x8f, 0xc5, 0x65, + 0x1d, 0xe7, 0x8a, 0x72, 0x9c, 0xb5, 0x18, 0x99, 0x70, 0x9f, 0x04, 0xbf, 0xfe, 0xf3, 0x32, 0x54, + 0xe4, 0x1b, 0x9d, 0xf4, 0xd3, 0x1d, 0x20, 0x21, 0x55, 0x6d, 0x52, 0xce, 0xaa, 0x3d, 0x46, 0xd7, + 0xea, 0xeb, 0xd9, 0x36, 0xca, 0xa0, 0x71, 0xfa, 0x62, 0xa3, 0x8a, 0x57, 0xd2, 0xfd, 0xfb, 0x71, + 0x4f, 0x65, 0x59, 0xcb, 0x21, 0x6c, 0xe0, 0x94, 0x5e, 0xb9, 0x81, 0x73, 0x09, 0xaa, 0xce, 0x7c, + 0x66, 0xf2, 0x45, 0xa0, 0x12, 0x6e, 0xc5, 0x99, 0xcf, 0x1e, 0x2f, 0xd0, 0x4b, 0xb8, 0xcb, 0xe9, + 0x14, 0x49, 0x32, 0xdd, 0xd6, 0x70, 0x42, 0x10, 0x77, 0xa0, 0x99, 0xb8, 0xb9, 0xdb, 0x96, 0x7a, + 0x01, 0xb6, 0x92, 0x8e, 0xbf, 0x7f, 0x5f, 0x69, 0xd9, 0x88, 0x6e, 0xf2, 0xfb, 0x16, 0xb9, 0x91, + 0xee, 0x57, 0xe0, 0x85, 0xbf, 0x86, 0x31, 0x96, 0x68, 0x49, 0x88, 0xeb, 0xbe, 0xd8, 0x80, 0x88, + 0x3a, 0x09, 0xa9, 0x23, 0xa4, 0x26, 0x26, 0x90, 0xf8, 0x26, 0xb4, 0xe3, 0x3b, 0xb3, 0x84, 0x80, + 0x94, 0x12, 0x4f, 0x23, 0xf0, 0x1d, 0x58, 0x73, 0xd8, 0x82, 0x9b, 0x59, 0x74, 0x03, 0xd1, 0x44, + 0xd0, 0x0e, 0xd3, 0x1c, 0xdf, 0x81, 0x56, 0x9c, 0x97, 0x10, 0xbb, 0x2a, 0xbb, 0x46, 0xd1, 0x2c, + 0xc2, 0x2e, 0x43, 0x2d, 0x7a, 0xb1, 0x34, 0x11, 0x50, 0xa5, 0xf2, 0xa1, 0x12, 0xbd, 0x81, 0x7c, + 0x16, 0xcc, 0xa7, 0x5c, 0x09, 0x69, 0x21, 0x06, 0xdf, 0x40, 0x86, 0x9c, 0x47, 0xec, 0x35, 0x68, + 0x86, 0x11, 0x2e, 0x71, 0x6d, 0xc4, 0xad, 0x86, 0x93, 0x08, 0xda, 0x84, 0x8e, 0xe7, 0xbb, 0x9e, + 0x1b, 0x30, 0xdf, 0xa4, 0x96, 0xe5, 0xb3, 0x20, 0xe8, 0x76, 0xa4, 0xbc, 0x70, 0x7e, 0x57, 0x4e, + 0xeb, 0xef, 0x42, 0x35, 0x7c, 0x8a, 0xad, 0x41, 0x19, 0xad, 0x8e, 0x2e, 0x58, 0x32, 0xe4, 0x40, + 0x94, 0xe2, 0x5d, 0xcf, 0x53, 0x8d, 0x47, 0xf1, 0xa9, 0xff, 0x0c, 0xaa, 0xea, 0xc0, 0x72, 0xdb, + 0x51, 0xdf, 0x87, 0x55, 0x8f, 0xfa, 0x42, 0x8d, 0x64, 0x53, 0x2a, 0x7c, 0xec, 0x1f, 0x50, 0x9f, + 0x3f, 0x62, 0x3c, 0xd5, 0x9b, 0x6a, 0x20, 0x5e, 0x4e, 0xe9, 0x77, 0xa0, 0x99, 0xc2, 0x88, 0x6d, + 0xa1, 0x1f, 0x85, 0x41, 0x8d, 0x83, 0x68, 0xe5, 0x42, 0xbc, 0xb2, 0x7e, 0x17, 0xea, 0xd1, 0xd9, + 0x88, 0x37, 0x69, 0xa8, 0xba, 0xa6, 0xcc, 0x2d, 0x87, 0xd8, 0x6f, 0x73, 0x9f, 0x31, 0x5f, 0xc5, + 0x84, 0x1c, 0xe8, 0x4f, 0x12, 0x49, 0x48, 0x96, 0x08, 0x72, 0x0b, 0xaa, 0x2a, 0x09, 0xa9, 0xa8, + 0x0c, 0x3b, 0x6b, 0x07, 0x98, 0x85, 0xc2, 0xce, 0x9a, 0xcc, 0x49, 0xb1, 0xd8, 0x42, 0x52, 0xec, + 0x14, 0x6a, 0x61, 0xa2, 0x49, 0x67, 0x64, 0x29, 0xb1, 0x93, 0xcd, 0xc8, 0x4a, 0x68, 0x0c, 0x14, + 0xde, 0x11, 0xd8, 0x63, 0x87, 0x59, 0x66, 0x1c, 0x42, 0xb8, 0x46, 0xcd, 0x68, 0x4b, 0xc2, 0xc3, + 0x30, 0x5e, 0xf4, 0x77, 0xa0, 0x22, 0xf7, 0x96, 0x9b, 0xbe, 0xf2, 0x6a, 0xd4, 0xef, 0x34, 0xa8, + 0x85, 0x79, 0x3a, 0x97, 0x29, 0xb5, 0xe9, 0xc2, 0xd7, 0xdd, 0xf4, 0xff, 0x3e, 0xf1, 0xdc, 0x02, + 0x22, 0xf3, 0xcb, 0xb1, 0xcb, 0x6d, 0x67, 0x6c, 0x4a, 0x5b, 0xcb, 0x1c, 0xd4, 0x41, 0xca, 0x21, + 0x12, 0x0e, 0xc4, 0xfc, 0x5b, 0xd7, 0xa0, 0x91, 0x68, 0x10, 0x92, 0x2a, 0x14, 0x3f, 0x61, 0xcf, + 0x3a, 0x2b, 0xa4, 0x01, 0x55, 0x83, 0x61, 0xbb, 0xa7, 0xa3, 0x6d, 0x7f, 0x56, 0x86, 0xf6, 0xee, + 0xe0, 0xde, 0xfe, 0xae, 0xe7, 0x4d, 0xed, 0x11, 0xc5, 0xfe, 0xc0, 0x16, 0x94, 0xb0, 0x45, 0x92, + 0xf3, 0x53, 0x50, 0x2f, 0xaf, 0x57, 0x47, 0xb6, 0xa1, 0x8c, 0x9d, 0x12, 0x92, 0xf7, 0x8b, 0x50, + 0x2f, 0xb7, 0x65, 0x27, 0x16, 0x91, 0xbd, 0x94, 0xb3, 0x3f, 0x0c, 0xf5, 0xf2, 0xfa, 0x76, 0xe4, + 0x43, 0xa8, 0xc7, 0x2d, 0x8c, 0x65, 0x3f, 0x0f, 0xf5, 0x96, 0x76, 0xf0, 0x04, 0x7f, 0xfc, 0x14, + 0x59, 0xf6, 0x2b, 0x47, 0x6f, 0x69, 0xab, 0x8b, 0xec, 0x40, 0x35, 0x7c, 0xc4, 0xe6, 0xff, 0x80, + 0xd3, 0x5b, 0xd2, 0x5d, 0x13, 0xe6, 0x91, 0x5d, 0x89, 0xbc, 0x5f, 0x99, 0x7a, 0xb9, 0x2d, 0x40, + 0x72, 0x1b, 0x2a, 0xea, 0x56, 0x95, 0xfb, 0x23, 0x4e, 0x2f, 0xbf, 0x47, 0x26, 0x94, 0x8c, 0xfb, + 0x32, 0xcb, 0x7e, 0x09, 0xeb, 0x2d, 0xed, 0x55, 0x92, 0x5d, 0x80, 0x44, 0x73, 0x61, 0xe9, 0x4f, + 0x5c, 0xbd, 0xe5, 0x3d, 0x48, 0x72, 0x17, 0x6a, 0x71, 0x5f, 0x39, 0xff, 0x47, 0xab, 0xde, 0xb2, + 0xb6, 0xe0, 0xe0, 0xca, 0x3f, 0xfe, 0xb8, 0xae, 0xfd, 0xf2, 0x74, 0x5d, 0xfb, 0xe2, 0x74, 0x5d, + 0xfb, 0xf2, 0x74, 0x5d, 0xfb, 0xed, 0xe9, 0xba, 0xf6, 0x87, 0xd3, 0x75, 0xed, 0xd7, 0x7f, 0x5a, + 0xd7, 0x86, 0x15, 0x8c, 0x91, 0xf7, 0xfe, 0x13, 0x00, 0x00, 0xff, 0xff, 0x3d, 0x1d, 0xa3, 0x8f, + 0xa4, 0x1d, 0x00, 0x00, } diff --git a/abci/types/types.proto b/abci/types/types.proto index 19d177ebb..b52c612b6 100644 --- a/abci/types/types.proto +++ b/abci/types/types.proto @@ -174,6 +174,10 @@ message ResponseBeginBlock { repeated Event events = 1 [(gogoproto.nullable)=false, (gogoproto.jsontag)="events,omitempty"]; } +message ResponseBeginBlockDeprecated { + repeated common.KVPair tags = 1 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; +} + message ResponseCheckTx { uint32 code = 1; bytes data = 2; @@ -224,6 +228,12 @@ message ResponseEndBlock { repeated Event events = 3 [(gogoproto.nullable)=false, (gogoproto.jsontag)="events,omitempty"]; } +message ResponseEndBlockDeprecated { + repeated ValidatorUpdate validator_updates = 1 [(gogoproto.nullable)=false]; + ConsensusParams consensus_param_updates = 2; + repeated common.KVPair tags = 3 [(gogoproto.nullable)=false, (gogoproto.jsontag)="tags,omitempty"]; +} + message ResponseCommit { // reserve 1 bytes data = 2; diff --git a/abci/types/typespb_test.go b/abci/types/typespb_test.go index e86e626e9..c289f48a7 100644 --- a/abci/types/typespb_test.go +++ b/abci/types/typespb_test.go @@ -1199,6 +1199,62 @@ func TestResponseBeginBlockMarshalTo(t *testing.T) { } } +func TestResponseBeginBlockDeprecatedProto(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseBeginBlockDeprecated(popr, false) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseBeginBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + littlefuzz := make([]byte, len(dAtA)) + copy(littlefuzz, dAtA) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } + if len(littlefuzz) > 0 { + fuzzamount := 100 + for i := 0; i < fuzzamount; i++ { + littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256)) + littlefuzz = append(littlefuzz, byte(popr.Intn(256))) + } + // shouldn't panic + _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg) + } +} + +func TestResponseBeginBlockDeprecatedMarshalTo(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseBeginBlockDeprecated(popr, false) + size := p.Size() + dAtA := make([]byte, size) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + _, err := p.MarshalTo(dAtA) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseBeginBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseCheckTxProto(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -1479,6 +1535,62 @@ func TestResponseEndBlockMarshalTo(t *testing.T) { } } +func TestResponseEndBlockDeprecatedProto(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseEndBlockDeprecated(popr, false) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseEndBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + littlefuzz := make([]byte, len(dAtA)) + copy(littlefuzz, dAtA) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } + if len(littlefuzz) > 0 { + fuzzamount := 100 + for i := 0; i < fuzzamount; i++ { + littlefuzz[popr.Intn(len(littlefuzz))] = byte(popr.Intn(256)) + littlefuzz = append(littlefuzz, byte(popr.Intn(256))) + } + // shouldn't panic + _ = github_com_gogo_protobuf_proto.Unmarshal(littlefuzz, msg) + } +} + +func TestResponseEndBlockDeprecatedMarshalTo(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseEndBlockDeprecated(popr, false) + size := p.Size() + dAtA := make([]byte, size) + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + _, err := p.MarshalTo(dAtA) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseEndBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.Unmarshal(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + for i := range dAtA { + dAtA[i] = byte(popr.Intn(256)) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseCommitProto(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -2753,6 +2865,24 @@ func TestResponseBeginBlockJSON(t *testing.T) { t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) } } +func TestResponseBeginBlockDeprecatedJSON(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseBeginBlockDeprecated(popr, true) + marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{} + jsondata, err := marshaler.MarshalToString(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseBeginBlockDeprecated{} + err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) + } +} func TestResponseCheckTxJSON(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -2843,6 +2973,24 @@ func TestResponseEndBlockJSON(t *testing.T) { t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) } } +func TestResponseEndBlockDeprecatedJSON(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseEndBlockDeprecated(popr, true) + marshaler := github_com_gogo_protobuf_jsonpb.Marshaler{} + jsondata, err := marshaler.MarshalToString(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + msg := &ResponseEndBlockDeprecated{} + err = github_com_gogo_protobuf_jsonpb.UnmarshalString(jsondata, msg) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Json Equal %#v", seed, msg, p) + } +} func TestResponseCommitJSON(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -3719,6 +3867,34 @@ func TestResponseBeginBlockProtoCompactText(t *testing.T) { } } +func TestResponseBeginBlockDeprecatedProtoText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseBeginBlockDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p) + msg := &ResponseBeginBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + +func TestResponseBeginBlockDeprecatedProtoCompactText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseBeginBlockDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.CompactTextString(p) + msg := &ResponseBeginBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseCheckTxProtoText(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -3859,6 +4035,34 @@ func TestResponseEndBlockProtoCompactText(t *testing.T) { } } +func TestResponseEndBlockDeprecatedProtoText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseEndBlockDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.MarshalTextString(p) + msg := &ResponseEndBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + +func TestResponseEndBlockDeprecatedProtoCompactText(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseEndBlockDeprecated(popr, true) + dAtA := github_com_gogo_protobuf_proto.CompactTextString(p) + msg := &ResponseEndBlockDeprecated{} + if err := github_com_gogo_protobuf_proto.UnmarshalText(dAtA, msg); err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + if !p.Equal(msg) { + t.Fatalf("seed = %d, %#v !Proto %#v", seed, msg, p) + } +} + func TestResponseCommitProtoText(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -4769,6 +4973,28 @@ func TestResponseBeginBlockSize(t *testing.T) { } } +func TestResponseBeginBlockDeprecatedSize(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseBeginBlockDeprecated(popr, true) + size2 := github_com_gogo_protobuf_proto.Size(p) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + size := p.Size() + if len(dAtA) != size { + t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA)) + } + if size2 != size { + t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2) + } + size3 := github_com_gogo_protobuf_proto.Size(p) + if size3 != size { + t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3) + } +} + func TestResponseCheckTxSize(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) @@ -4879,6 +5105,28 @@ func TestResponseEndBlockSize(t *testing.T) { } } +func TestResponseEndBlockDeprecatedSize(t *testing.T) { + seed := time.Now().UnixNano() + popr := math_rand.New(math_rand.NewSource(seed)) + p := NewPopulatedResponseEndBlockDeprecated(popr, true) + size2 := github_com_gogo_protobuf_proto.Size(p) + dAtA, err := github_com_gogo_protobuf_proto.Marshal(p) + if err != nil { + t.Fatalf("seed = %d, err = %v", seed, err) + } + size := p.Size() + if len(dAtA) != size { + t.Errorf("seed = %d, size %v != marshalled size %v", seed, size, len(dAtA)) + } + if size2 != size { + t.Errorf("seed = %d, size %v != before marshal proto.Size %v", seed, size, size2) + } + size3 := github_com_gogo_protobuf_proto.Size(p) + if size3 != size { + t.Errorf("seed = %d, size %v != after marshal proto.Size %v", seed, size, size3) + } +} + func TestResponseCommitSize(t *testing.T) { seed := time.Now().UnixNano() popr := math_rand.New(math_rand.NewSource(seed)) From 41d8dd27798e77d54d25e18623376f05288085e2 Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Tue, 5 Nov 2019 10:04:03 +0800 Subject: [PATCH 211/211] change field to lowercase and add ResponseEndBlockDeprecated ResponseBeginBlockDeprecated --- abci/types/util.go | 26 ++++++++++++++++++++++++++ state/store.go | 22 +++++++++++----------- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/abci/types/util.go b/abci/types/util.go index 2afcd2d75..c6db9b4e8 100644 --- a/abci/types/util.go +++ b/abci/types/util.go @@ -53,3 +53,29 @@ func ConvertDeprecatedDeliverTxResponse(deprecated *ResponseDeliverTxDeprecated) XXX_sizecache: deprecated.XXX_sizecache, } } + +func ConvertDeprecatedBeginBlockResponse(deprecated *ResponseBeginBlockDeprecated) *ResponseBeginBlock { + if deprecated == nil { + return nil + } + return &ResponseBeginBlock{ + Events: []Event{{Attributes: deprecated.Tags}}, + XXX_NoUnkeyedLiteral: deprecated.XXX_NoUnkeyedLiteral, + XXX_unrecognized: deprecated.XXX_unrecognized, + XXX_sizecache: deprecated.XXX_sizecache, + } +} + +func ConvertDeprecatedEndBlockResponse(deprecated *ResponseEndBlockDeprecated) *ResponseEndBlock { + if deprecated == nil { + return nil + } + return &ResponseEndBlock{ + ValidatorUpdates: deprecated.ValidatorUpdates, + ConsensusParamUpdates: deprecated.ConsensusParamUpdates, + Events: []Event{{Attributes: deprecated.Tags}}, + XXX_NoUnkeyedLiteral: deprecated.XXX_NoUnkeyedLiteral, + XXX_unrecognized: deprecated.XXX_unrecognized, + XXX_sizecache: deprecated.XXX_sizecache, + } +} diff --git a/state/store.go b/state/store.go index 2b23a05e3..5a6b1cbf5 100644 --- a/state/store.go +++ b/state/store.go @@ -140,15 +140,15 @@ func saveState(db dbm.DB, state State, key []byte) { // of the various ABCI calls during block processing. // It is persisted to disk for each height before calling Commit. type ABCIResponses struct { - DeliverTx []*abci.ResponseDeliverTx `json:"deliver_tx"` - EndBlock *abci.ResponseEndBlock `json:"end_block"` - BeginBlock *abci.ResponseBeginBlock `json:"begin_block"` + DeliverTx []*abci.ResponseDeliverTx + EndBlock *abci.ResponseEndBlock + BeginBlock *abci.ResponseBeginBlock } type ABCIResponsesDeprecated struct { - DeliverTx []*abci.ResponseDeliverTxDeprecated `json:"deliver_tx"` - EndBlock *abci.ResponseEndBlock `json:"end_block"` - BeginBlock *abci.ResponseBeginBlock `json:"begin_block"` + DeliverTx []*abci.ResponseDeliverTxDeprecated + EndBlock *abci.ResponseEndBlockDeprecated + BeginBlock *abci.ResponseBeginBlockDeprecated } // NewABCIResponses returns a new ABCIResponses @@ -185,21 +185,21 @@ func LoadABCIResponses(db dbm.DB, height int64) (*ABCIResponses, error) { abciResponses := new(ABCIResponses) err := cdc.UnmarshalBinaryBare(buf, abciResponses) if err != nil { - abciResponsesDeprecated := new(ABCIResponsesDeprecated) - err := cdc.UnmarshalBinaryBare(buf, abciResponsesDeprecated) + deprecated := new(ABCIResponsesDeprecated) + err := cdc.UnmarshalBinaryBare(buf, deprecated) if err != nil { // DATA HAS BEEN CORRUPTED OR THE SPEC HAS CHANGED cmn.Exit(fmt.Sprintf(`LoadABCIResponses: Data has been corrupted or its spec has changed: %v\n`, err)) } var deliverTxs []*abci.ResponseDeliverTx - for _, result := range abciResponsesDeprecated.DeliverTx { + for _, result := range deprecated.DeliverTx { deliverTxs = append(deliverTxs, abci.ConvertDeprecatedDeliverTxResponse(result)) } return &ABCIResponses{ DeliverTx: deliverTxs, - EndBlock: abciResponsesDeprecated.EndBlock, - BeginBlock: abciResponsesDeprecated.BeginBlock, + EndBlock: abci.ConvertDeprecatedEndBlockResponse(deprecated.EndBlock), + BeginBlock: abci.ConvertDeprecatedBeginBlockResponse(deprecated.BeginBlock), }, nil } // TODO: ensure that buf is completely read.

@lDVVp65ap=!a592}az0Eq^2Lifb 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 Date: Thu, 8 Aug 2019 12:31:13 +0200 Subject: [PATCH 172/211] replace errors.go with github.com/pkg/errors (1/2) (#3888) * (1/2) of replace errors.go with github.com/pkg/errors ref #3862 - step one in removing instances of errors.go in favor of github.com/pkg/errors Signed-off-by: Marko Baricevic * gofmt * add in /store --- blockchain/v0/reactor_test.go | 8 ++++---- blockchain/v1/reactor_test.go | 8 ++++---- crypto/merkle/proof.go | 16 ++++++++-------- crypto/merkle/proof_key_path.go | 8 ++++---- crypto/merkle/proof_simple_value.go | 11 ++++++----- crypto/merkle/proof_test.go | 8 ++++---- p2p/switch.go | 2 +- p2p/test_util.go | 4 +++- privval/messages.go | 2 +- store/store.go | 13 ++++++------- store/store_test.go | 17 +++-------------- types/block.go | 2 +- types/genesis.go | 14 ++++++++------ 13 files changed, 53 insertions(+), 60 deletions(-) diff --git a/blockchain/v0/reactor_test.go b/blockchain/v0/reactor_test.go index cf488af29..43b707f4b 100644 --- a/blockchain/v0/reactor_test.go +++ b/blockchain/v0/reactor_test.go @@ -6,13 +6,13 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/tendermint/tendermint/store" "github.com/stretchr/testify/assert" abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" - cmn "github.com/tendermint/tendermint/libs/common" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" @@ -60,7 +60,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals proxyApp := proxy.NewAppConns(cc) err := proxyApp.Start() if err != nil { - panic(cmn.ErrorWrap(err, "error start app")) + panic(errors.Wrap(err, "error start app")) } blockDB := dbm.NewMemDB() @@ -69,7 +69,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) if err != nil { - panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + panic(errors.Wrap(err, "error constructing state from genesis file")) } // Make the BlockchainReactor itself. @@ -103,7 +103,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals state, err = blockExec.ApplyBlock(state, blockID, thisBlock) if err != nil { - panic(cmn.ErrorWrap(err, "error apply block")) + panic(errors.Wrap(err, "error apply block")) } blockStore.SaveBlock(thisBlock, thisParts, lastCommit) diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go index 4b9614d2d..5e68c1ce1 100644 --- a/blockchain/v1/reactor_test.go +++ b/blockchain/v1/reactor_test.go @@ -8,10 +8,10 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" - cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" "github.com/tendermint/tendermint/mock" "github.com/tendermint/tendermint/p2p" @@ -78,7 +78,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals proxyApp := proxy.NewAppConns(cc) err := proxyApp.Start() if err != nil { - panic(cmn.ErrorWrap(err, "error start app")) + panic(errors.Wrap(err, "error start app")) } blockDB := dbm.NewMemDB() @@ -87,7 +87,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals state, err := sm.LoadStateFromDBOrGenesisDoc(stateDB, genDoc) if err != nil { - panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + panic(errors.Wrap(err, "error constructing state from genesis file")) } // Make the BlockchainReactor itself. @@ -117,7 +117,7 @@ func newBlockchainReactor(logger log.Logger, genDoc *types.GenesisDoc, privVals state, err = blockExec.ApplyBlock(state, blockID, thisBlock) if err != nil { - panic(cmn.ErrorWrap(err, "error apply block")) + panic(errors.Wrap(err, "error apply block")) } blockStore.SaveBlock(thisBlock, thisParts, lastCommit) diff --git a/crypto/merkle/proof.go b/crypto/merkle/proof.go index 5e2a3ab12..ad101d94d 100644 --- a/crypto/merkle/proof.go +++ b/crypto/merkle/proof.go @@ -3,7 +3,7 @@ package merkle import ( "bytes" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" ) //---------------------------------------- @@ -44,11 +44,11 @@ func (poz ProofOperators) Verify(root []byte, keypath string, args [][]byte) (er key := op.GetKey() if len(key) != 0 { if len(keys) == 0 { - return cmn.NewError("Key path has insufficient # of parts: expected no more keys but got %+v", string(key)) + return errors.Errorf("Key path has insufficient # of parts: expected no more keys but got %+v", string(key)) } lastKey := keys[len(keys)-1] if !bytes.Equal(lastKey, key) { - return cmn.NewError("Key mismatch on operation #%d: expected %+v but got %+v", i, string(lastKey), string(key)) + return errors.Errorf("Key mismatch on operation #%d: expected %+v but got %+v", i, string(lastKey), string(key)) } keys = keys[:len(keys)-1] } @@ -58,10 +58,10 @@ func (poz ProofOperators) Verify(root []byte, keypath string, args [][]byte) (er } } if !bytes.Equal(root, args[0]) { - return cmn.NewError("Calculated root hash is invalid: expected %+v but got %+v", root, args[0]) + return errors.Errorf("Calculated root hash is invalid: expected %+v but got %+v", root, args[0]) } if len(keys) != 0 { - return cmn.NewError("Keypath not consumed all") + return errors.New("Keypath not consumed all") } return nil } @@ -92,7 +92,7 @@ func (prt *ProofRuntime) RegisterOpDecoder(typ string, dec OpDecoder) { func (prt *ProofRuntime) Decode(pop ProofOp) (ProofOperator, error) { decoder := prt.decoders[pop.Type] if decoder == nil { - return nil, cmn.NewError("unrecognized proof type %v", pop.Type) + return nil, errors.Errorf("unrecognized proof type %v", pop.Type) } return decoder(pop) } @@ -102,7 +102,7 @@ func (prt *ProofRuntime) DecodeProof(proof *Proof) (ProofOperators, error) { for _, pop := range proof.Ops { operator, err := prt.Decode(pop) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding a proof operator") + return nil, errors.Wrap(err, "decoding a proof operator") } poz = append(poz, operator) } @@ -122,7 +122,7 @@ func (prt *ProofRuntime) VerifyAbsence(proof *Proof, root []byte, keypath string func (prt *ProofRuntime) Verify(proof *Proof, root []byte, keypath string, args [][]byte) (err error) { poz, err := prt.DecodeProof(proof) if err != nil { - return cmn.ErrorWrap(err, "decoding proof") + return errors.Wrap(err, "decoding proof") } return poz.Verify(root, keypath, args) } diff --git a/crypto/merkle/proof_key_path.go b/crypto/merkle/proof_key_path.go index aec93e826..7ea67853b 100644 --- a/crypto/merkle/proof_key_path.go +++ b/crypto/merkle/proof_key_path.go @@ -6,7 +6,7 @@ import ( "net/url" "strings" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" ) /* @@ -87,7 +87,7 @@ func (pth KeyPath) String() string { // Each key must use a known encoding. func KeyPathToKeys(path string) (keys [][]byte, err error) { if path == "" || path[0] != '/' { - return nil, cmn.NewError("key path string must start with a forward slash '/'") + return nil, errors.New("key path string must start with a forward slash '/'") } parts := strings.Split(path[1:], "/") keys = make([][]byte, len(parts)) @@ -96,13 +96,13 @@ func KeyPathToKeys(path string) (keys [][]byte, err error) { hexPart := part[2:] key, err := hex.DecodeString(hexPart) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding hex-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding hex-encoded part #%d: /%s", i, part) } keys[i] = key } else { key, err := url.PathUnescape(part) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding url-encoded part #%d: /%s", i, part) + return nil, errors.Wrapf(err, "decoding url-encoded part #%d: /%s", i, part) } keys[i] = []byte(key) // TODO Test this with random bytes, I'm not sure that it works for arbitrary bytes... } diff --git a/crypto/merkle/proof_simple_value.go b/crypto/merkle/proof_simple_value.go index 247921ad5..2c89bb5fd 100644 --- a/crypto/merkle/proof_simple_value.go +++ b/crypto/merkle/proof_simple_value.go @@ -4,8 +4,9 @@ import ( "bytes" "fmt" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto/tmhash" - cmn "github.com/tendermint/tendermint/libs/common" ) const ProofOpSimpleValue = "simple:v" @@ -39,12 +40,12 @@ func NewSimpleValueOp(key []byte, proof *SimpleProof) SimpleValueOp { func SimpleValueOpDecoder(pop ProofOp) (ProofOperator, error) { if pop.Type != ProofOpSimpleValue { - return nil, cmn.NewError("unexpected ProofOp.Type; got %v, want %v", pop.Type, ProofOpSimpleValue) + return nil, errors.Errorf("unexpected ProofOp.Type; got %v, want %v", pop.Type, ProofOpSimpleValue) } var op SimpleValueOp // a bit strange as we'll discard this, but it works. err := cdc.UnmarshalBinaryLengthPrefixed(pop.Data, &op) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding ProofOp.Data into SimpleValueOp") + return nil, errors.Wrap(err, "decoding ProofOp.Data into SimpleValueOp") } return NewSimpleValueOp(pop.Key, op.Proof), nil } @@ -64,7 +65,7 @@ func (op SimpleValueOp) String() string { func (op SimpleValueOp) Run(args [][]byte) ([][]byte, error) { if len(args) != 1 { - return nil, cmn.NewError("expected 1 arg, got %v", len(args)) + return nil, errors.Errorf("expected 1 arg, got %v", len(args)) } value := args[0] hasher := tmhash.New() @@ -78,7 +79,7 @@ func (op SimpleValueOp) Run(args [][]byte) ([][]byte, error) { kvhash := leafHash(bz.Bytes()) if !bytes.Equal(kvhash, op.Proof.LeafHash) { - return nil, cmn.NewError("leaf hash mismatch: want %X got %X", op.Proof.LeafHash, kvhash) + return nil, errors.Errorf("leaf hash mismatch: want %X got %X", op.Proof.LeafHash, kvhash) } return [][]byte{ diff --git a/crypto/merkle/proof_test.go b/crypto/merkle/proof_test.go index 4de3246f1..4dc916ac9 100644 --- a/crypto/merkle/proof_test.go +++ b/crypto/merkle/proof_test.go @@ -3,9 +3,9 @@ package merkle import ( "testing" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" amino "github.com/tendermint/go-amino" - cmn "github.com/tendermint/tendermint/libs/common" ) const ProofOpDomino = "test:domino" @@ -34,7 +34,7 @@ func DominoOpDecoder(pop ProofOp) (ProofOperator, error) { var op DominoOp // a bit strange as we'll discard this, but it works. err := amino.UnmarshalBinaryLengthPrefixed(pop.Data, &op) if err != nil { - return nil, cmn.ErrorWrap(err, "decoding ProofOp.Data into SimpleValueOp") + return nil, errors.Wrap(err, "decoding ProofOp.Data into SimpleValueOp") } return NewDominoOp(string(pop.Key), op.Input, op.Output), nil } @@ -50,10 +50,10 @@ func (dop DominoOp) ProofOp() ProofOp { func (dop DominoOp) Run(input [][]byte) (output [][]byte, err error) { if len(input) != 1 { - return nil, cmn.NewError("Expected input of length 1") + return nil, errors.New("Expected input of length 1") } if string(input[0]) != dop.Input { - return nil, cmn.NewError("Expected input %v, got %v", + return nil, errors.Errorf("Expected input %v, got %v", dop.Input, string(input[0])) } return [][]byte{[]byte(dop.Output)}, nil diff --git a/p2p/switch.go b/p2p/switch.go index 6f4a3a193..274774003 100644 --- a/p2p/switch.go +++ b/p2p/switch.go @@ -223,7 +223,7 @@ func (sw *Switch) OnStart() error { for _, reactor := range sw.reactors { err := reactor.Start() if err != nil { - return cmn.ErrorWrap(err, "failed to start %v", reactor) + return errors.Wrapf(err, "failed to start %v", reactor) } } diff --git a/p2p/test_util.go b/p2p/test_util.go index fa175aeb4..a14073f99 100644 --- a/p2p/test_util.go +++ b/p2p/test_util.go @@ -5,6 +5,8 @@ import ( "net" "time" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/crypto/ed25519" cmn "github.com/tendermint/tendermint/libs/common" @@ -233,7 +235,7 @@ func testPeerConn( // Encrypt connection conn, err = upgradeSecretConn(conn, cfg.HandshakeTimeout, ourNodePrivKey) if err != nil { - return pc, cmn.ErrorWrap(err, "Error creating peer") + return pc, errors.Wrap(err, "Error creating peer") } // Only the information we already have diff --git a/privval/messages.go b/privval/messages.go index 7704049fe..c172a5ea1 100644 --- a/privval/messages.go +++ b/privval/messages.go @@ -1,7 +1,7 @@ package privval import ( - "github.com/tendermint/go-amino" + amino "github.com/tendermint/go-amino" "github.com/tendermint/tendermint/crypto" "github.com/tendermint/tendermint/types" ) diff --git a/store/store.go b/store/store.go index 9125d15e3..9a4abdb10 100644 --- a/store/store.go +++ b/store/store.go @@ -4,9 +4,8 @@ import ( "fmt" "sync" - cmn "github.com/tendermint/tendermint/libs/common" + "github.com/pkg/errors" dbm "github.com/tendermint/tendermint/libs/db" - "github.com/tendermint/tendermint/types" ) @@ -67,7 +66,7 @@ func (bs *BlockStore) LoadBlock(height int64) *types.Block { if err != nil { // NOTE: The existence of meta should imply the existence of the // block. So, make sure meta is only saved after blocks are saved. - panic(cmn.ErrorWrap(err, "Error reading block")) + panic(errors.Wrap(err, "Error reading block")) } return block } @@ -83,7 +82,7 @@ func (bs *BlockStore) LoadBlockPart(height int64, index int) *types.Part { } err := cdc.UnmarshalBinaryBare(bz, part) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block part")) + panic(errors.Wrap(err, "Error reading block part")) } return part } @@ -98,7 +97,7 @@ func (bs *BlockStore) LoadBlockMeta(height int64) *types.BlockMeta { } err := cdc.UnmarshalBinaryBare(bz, blockMeta) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block meta")) + panic(errors.Wrap(err, "Error reading block meta")) } return blockMeta } @@ -115,7 +114,7 @@ func (bs *BlockStore) LoadBlockCommit(height int64) *types.Commit { } err := cdc.UnmarshalBinaryBare(bz, commit) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block commit")) + panic(errors.Wrap(err, "Error reading block commit")) } return commit } @@ -131,7 +130,7 @@ func (bs *BlockStore) LoadSeenCommit(height int64) *types.Commit { } err := cdc.UnmarshalBinaryBare(bz, commit) if err != nil { - panic(cmn.ErrorWrap(err, "Error reading block seen commit")) + panic(errors.Wrap(err, "Error reading block seen commit")) } return commit } diff --git a/store/store_test.go b/store/store_test.go index 836737b72..f7d6c1cd2 100644 --- a/store/store_test.go +++ b/store/store_test.go @@ -9,10 +9,11 @@ import ( "testing" "time" + "github.com/pkg/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + cfg "github.com/tendermint/tendermint/config" - cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/db" dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/log" @@ -32,18 +33,6 @@ func makeTestCommit(height int64, timestamp time.Time) *types.Commit { return types.NewCommit(types.BlockID{}, commitSigs) } -func makeTxs(height int64) (txs []types.Tx) { - for i := 0; i < 10; i++ { - txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) - } - return txs -} - -func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { - block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) - return block -} - func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFunc) { config := cfg.ResetTestRoot("blockchain_reactor_test") // blockDB := dbm.NewDebugDB("blockDB", dbm.NewMemDB()) @@ -52,7 +41,7 @@ func makeStateAndBlockStore(logger log.Logger) (sm.State, *BlockStore, cleanupFu stateDB := dbm.NewMemDB() state, err := sm.LoadStateFromDBOrGenesisFile(stateDB, config.GenesisFile()) if err != nil { - panic(cmn.ErrorWrap(err, "error constructing state from genesis file")) + panic(errors.Wrap(err, "error constructing state from genesis file")) } return state, NewBlockStore(blockDB), func() { os.RemoveAll(config.RootDir) } } diff --git a/types/block.go b/types/block.go index 8ad16a3de..18356417e 100644 --- a/types/block.go +++ b/types/block.go @@ -748,7 +748,7 @@ func (sh SignedHeader) ValidateBasic(chainID string) error { // ValidateBasic on the Commit. err := sh.Commit.ValidateBasic() if err != nil { - return cmn.ErrorWrap(err, "commit.ValidateBasic failed during SignedHeader.ValidateBasic") + return errors.Wrap(err, "commit.ValidateBasic failed during SignedHeader.ValidateBasic") } return nil } diff --git a/types/genesis.go b/types/genesis.go index de59fc87e..94680bca8 100644 --- a/types/genesis.go +++ b/types/genesis.go @@ -7,6 +7,8 @@ import ( "io/ioutil" "time" + "github.com/pkg/errors" + "github.com/tendermint/tendermint/crypto" cmn "github.com/tendermint/tendermint/libs/common" tmtime "github.com/tendermint/tendermint/types/time" @@ -64,10 +66,10 @@ func (genDoc *GenesisDoc) ValidatorHash() []byte { // and fills in defaults for optional fields left empty func (genDoc *GenesisDoc) ValidateAndComplete() error { if genDoc.ChainID == "" { - return cmn.NewError("Genesis doc must include non-empty chain_id") + return errors.New("Genesis doc must include non-empty chain_id") } if len(genDoc.ChainID) > MaxChainIDLen { - return cmn.NewError("chain_id in genesis doc is too long (max: %d)", MaxChainIDLen) + return errors.Errorf("chain_id in genesis doc is too long (max: %d)", MaxChainIDLen) } if genDoc.ConsensusParams == nil { @@ -78,10 +80,10 @@ func (genDoc *GenesisDoc) ValidateAndComplete() error { for i, v := range genDoc.Validators { if v.Power == 0 { - return cmn.NewError("The genesis file cannot contain validators with no voting power: %v", v) + return errors.Errorf("The genesis file cannot contain validators with no voting power: %v", v) } if len(v.Address) > 0 && !bytes.Equal(v.PubKey.Address(), v.Address) { - return cmn.NewError("Incorrect address for validator %v in the genesis file, should be %v", v, v.PubKey.Address()) + return errors.Errorf("Incorrect address for validator %v in the genesis file, should be %v", v, v.PubKey.Address()) } if len(v.Address) == 0 { genDoc.Validators[i].Address = v.PubKey.Address() @@ -117,11 +119,11 @@ func GenesisDocFromJSON(jsonBlob []byte) (*GenesisDoc, error) { func GenesisDocFromFile(genDocFile string) (*GenesisDoc, error) { jsonBlob, err := ioutil.ReadFile(genDocFile) if err != nil { - return nil, cmn.ErrorWrap(err, "Couldn't read GenesisDoc file") + return nil, errors.Wrap(err, "Couldn't read GenesisDoc file") } genDoc, err := GenesisDocFromJSON(jsonBlob) if err != nil { - return nil, cmn.ErrorWrap(err, fmt.Sprintf("Error reading GenesisDoc at %v", genDocFile)) + return nil, errors.Wrap(err, fmt.Sprintf("Error reading GenesisDoc at %v", genDocFile)) } return genDoc, nil } From f7f7364602d5e93a5405b29c6d6e2ef5d191cc96 Mon Sep 17 00:00:00 2001 From: Phil Salant Date: Sun, 11 Aug 2019 14:27:03 -0400 Subject: [PATCH 173/211] ValidateBasic tests (#3879) * Add test for bcBlockRequestMessage ValidateBasic method * Add test for bcNoBlockResponseMessage ValidateBasic method * Add test for bcStatusRequestMessage ValidateBasic method * Add test for bcStatusResponseMessage ValidateBasic method * Add blockchain v1 reactor ValidateBasic tests * Add test for NewRoundStepMessage ValidateBasic method * Add test for NewValidBlockMessage ValidateBasic method * Test BlockParts Size * Import cmn package * Add test for ProposalPOLMessage ValidateBasic method * Add test for BlockPartMessage ValidateBasic method * Add test for HasVoteMessage ValidateBasic method * Add test for VoteSetMaj23Message ValidateBasic method * Add test for VoteSetBitsMessage ValidateBasic method * Fix linter errors * Improve readability * Add test for BaseConfig ValidateBasic method * Add test for RPCConfig ValidateBasic method * Add test for P2PConfig ValidateBasic method * Add test for MempoolConfig ValidateBasic method * Add test for FastSyncConfig ValidateBasic method * Add test for ConsensusConfig ValidateBasic method * Add test for InstrumentationConfig ValidateBasic method * Add test for BlockID ValidateBasic method * Add test for SignedHeader ValidateBasic method * Add test for MockGoodEvidence and MockBadEvidence ValidateBasic methods * Remove debug logging Co-Authored-By: Marko * Update MempoolConfig field * Test a single struct field at a time, for maintainability Fixes #2740 --- blockchain/v0/reactor_test.go | 91 ++++++++++++ blockchain/v1/reactor_test.go | 76 ++++++++++ config/config_test.go | 114 +++++++++++++++ consensus/reactor_test.go | 252 ++++++++++++++++++++++++++++++++++ types/block_test.go | 88 ++++++++++++ types/evidence_test.go | 10 ++ 6 files changed, 631 insertions(+) diff --git a/blockchain/v0/reactor_test.go b/blockchain/v0/reactor_test.go index 43b707f4b..4ef9c6590 100644 --- a/blockchain/v0/reactor_test.go +++ b/blockchain/v0/reactor_test.go @@ -246,6 +246,97 @@ func TestBadBlockStopsPeer(t *testing.T) { assert.True(t, lastReactorPair.reactor.Switch.Peers().Size() < len(reactorPairs)-1) } +func TestBcBlockRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcBlockRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + nonResponseHeight int64 + expectErr bool + }{ + {"Valid Non-Response Message", 0, false}, + {"Valid Non-Response Message", 1, false}, + {"Invalid Non-Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + nonResponse := bcNoBlockResponseMessage{Height: tc.nonResponseHeight} + assert.Equal(t, tc.expectErr, nonResponse.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcStatusRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + responseHeight int64 + expectErr bool + }{ + {"Valid Response Message", 0, false}, + {"Valid Response Message", 1, false}, + {"Invalid Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + response := bcStatusResponseMessage{Height: tc.responseHeight} + assert.Equal(t, tc.expectErr, response.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +//---------------------------------------------- +// utility funcs + +func makeTxs(height int64) (txs []types.Tx) { + for i := 0; i < 10; i++ { + txs = append(txs, types.Tx([]byte{byte(height), byte(i)})) + } + return txs +} + +func makeBlock(height int64, state sm.State, lastCommit *types.Commit) *types.Block { + block, _ := state.MakeBlock(height, makeTxs(height), lastCommit, nil, state.Validators.GetProposer().Address) + return block +} + type testApp struct { abci.BaseApplication } diff --git a/blockchain/v1/reactor_test.go b/blockchain/v1/reactor_test.go index 5e68c1ce1..5f01c852a 100644 --- a/blockchain/v1/reactor_test.go +++ b/blockchain/v1/reactor_test.go @@ -317,6 +317,82 @@ outerFor: assert.True(t, lastReactorPair.bcR.Switch.Peers().Size() < len(reactorPairs)-1) } +func TestBcBlockRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcBlockRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcNoBlockResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + nonResponseHeight int64 + expectErr bool + }{ + {"Valid Non-Response Message", 0, false}, + {"Valid Non-Response Message", 1, false}, + {"Invalid Non-Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + nonResponse := bcNoBlockResponseMessage{Height: tc.nonResponseHeight} + assert.Equal(t, tc.expectErr, nonResponse.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusRequestMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + requestHeight int64 + expectErr bool + }{ + {"Valid Request Message", 0, false}, + {"Valid Request Message", 1, false}, + {"Invalid Request Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + request := bcStatusRequestMessage{Height: tc.requestHeight} + assert.Equal(t, tc.expectErr, request.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBcStatusResponseMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + responseHeight int64 + expectErr bool + }{ + {"Valid Response Message", 0, false}, + {"Valid Response Message", 1, false}, + {"Invalid Response Message", -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + response := bcStatusResponseMessage{Height: tc.responseHeight} + assert.Equal(t, tc.expectErr, response.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + //---------------------------------------------- // utility funcs diff --git a/config/config_test.go b/config/config_test.go index 6f9e3783e..6da032d07 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -1,6 +1,7 @@ package config import ( + "reflect" "testing" "time" @@ -52,3 +53,116 @@ func TestTLSConfiguration(t *testing.T) { cfg.RPC.TLSKeyFile = "/abs/path/to/file.key" assert.Equal("/abs/path/to/file.key", cfg.RPC.KeyFile()) } + +func TestBaseConfigValidateBasic(t *testing.T) { + cfg := TestBaseConfig() + assert.NoError(t, cfg.ValidateBasic()) + + // tamper with log format + cfg.LogFormat = "invalid" + assert.Error(t, cfg.ValidateBasic()) +} + +func TestRPCConfigValidateBasic(t *testing.T) { + cfg := TestRPCConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "GRPCMaxOpenConnections", + "MaxOpenConnections", + "MaxSubscriptionClients", + "MaxSubscriptionsPerClient", + "TimeoutBroadcastTxCommit", + "MaxBodyBytes", + "MaxHeaderBytes", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestP2PConfigValidateBasic(t *testing.T) { + cfg := TestP2PConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "MaxNumInboundPeers", + "MaxNumOutboundPeers", + "FlushThrottleTimeout", + "MaxPacketMsgPayloadSize", + "SendRate", + "RecvRate", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestMempoolConfigValidateBasic(t *testing.T) { + cfg := TestMempoolConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "Size", + "MaxTxsBytes", + "CacheSize", + "MaxTxBytes", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestFastSyncConfigValidateBasic(t *testing.T) { + cfg := TestFastSyncConfig() + assert.NoError(t, cfg.ValidateBasic()) + + // tamper with version + cfg.Version = "v1" + assert.NoError(t, cfg.ValidateBasic()) + + cfg.Version = "invalid" + assert.Error(t, cfg.ValidateBasic()) +} + +func TestConsensusConfigValidateBasic(t *testing.T) { + cfg := TestConsensusConfig() + assert.NoError(t, cfg.ValidateBasic()) + + fieldsToTest := []string{ + "TimeoutPropose", + "TimeoutProposeDelta", + "TimeoutPrevote", + "TimeoutPrevoteDelta", + "TimeoutPrecommit", + "TimeoutPrecommitDelta", + "TimeoutCommit", + "CreateEmptyBlocksInterval", + "PeerGossipSleepDuration", + "PeerQueryMaj23SleepDuration", + } + + for _, fieldName := range fieldsToTest { + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(-1) + assert.Error(t, cfg.ValidateBasic()) + reflect.ValueOf(cfg).Elem().FieldByName(fieldName).SetInt(0) + } +} + +func TestInstrumentationConfigValidateBasic(t *testing.T) { + cfg := TestInstrumentationConfig() + assert.NoError(t, cfg.ValidateBasic()) + + // tamper with maximum open connections + cfg.MaxOpenConnections = -1 + assert.Error(t, cfg.ValidateBasic()) +} diff --git a/consensus/reactor_test.go b/consensus/reactor_test.go index 3b487f757..e782064ef 100644 --- a/consensus/reactor_test.go +++ b/consensus/reactor_test.go @@ -19,6 +19,8 @@ import ( abci "github.com/tendermint/tendermint/abci/types" cfg "github.com/tendermint/tendermint/config" dbm "github.com/tendermint/tendermint/libs/db" + cstypes "github.com/tendermint/tendermint/consensus/types" + cmn "github.com/tendermint/tendermint/libs/common" "github.com/tendermint/tendermint/libs/log" mempl "github.com/tendermint/tendermint/mempool" "github.com/tendermint/tendermint/p2p" @@ -632,3 +634,253 @@ func capture() { count := runtime.Stack(trace, true) fmt.Printf("Stack of %d bytes: %s\n", count, trace) } + +//------------------------------------------------------------- +// Ensure basic validation of structs is functioning + +func TestNewRoundStepMessageValidateBasic(t *testing.T) { + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageStep cstypes.RoundStepType + messageLastCommitRound int + expectErr bool + }{ + {"Valid Message", 0, 0, 0x01, 1, false}, + {"Invalid Message", -1, 0, 0x01, 1, true}, + {"Invalid Message", 0, -1, 0x01, 1, true}, + {"Invalid Message", 0, 0, 0x00, 1, true}, + {"Invalid Message", 0, 0, 0x00, 0, true}, + {"Invalid Message", 1, 0, 0x01, 0, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := NewRoundStepMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Step: tc.messageStep, + LastCommitRound: tc.messageLastCommitRound, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestNewValidBlockMessageValidateBasic(t *testing.T) { + testBitArray := cmn.NewBitArray(1) + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageBlockParts *cmn.BitArray + expectErr bool + }{ + {"Valid Message", 0, 0, testBitArray, false}, + {"Invalid Message", -1, 0, testBitArray, true}, + {"Invalid Message", 0, -1, testBitArray, true}, + {"Invalid Message", 0, 0, cmn.NewBitArray(0), true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := NewValidBlockMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + BlockParts: tc.messageBlockParts, + } + + message.BlockPartsHeader.Total = 1 + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestProposalPOLMessageValidateBasic(t *testing.T) { + testBitArray := cmn.NewBitArray(1) + testCases := []struct { + testName string + messageHeight int64 + messageProposalPOLRound int + messageProposalPOL *cmn.BitArray + expectErr bool + }{ + {"Valid Message", 0, 0, testBitArray, false}, + {"Invalid Message", -1, 0, testBitArray, true}, + {"Invalid Message", 0, -1, testBitArray, true}, + {"Invalid Message", 0, 0, cmn.NewBitArray(0), true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := ProposalPOLMessage{ + Height: tc.messageHeight, + ProposalPOLRound: tc.messageProposalPOLRound, + ProposalPOL: tc.messageProposalPOL, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBlockPartMessageValidateBasic(t *testing.T) { + testPart := new(types.Part) + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messagePart *types.Part + expectErr bool + }{ + {"Valid Message", 0, 0, testPart, false}, + {"Invalid Message", -1, 0, testPart, true}, + {"Invalid Message", 0, -1, testPart, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := BlockPartMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Part: tc.messagePart, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } + + message := BlockPartMessage{Height: 0, Round: 0, Part: new(types.Part)} + message.Part.Index = -1 + + assert.Equal(t, true, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") +} + +func TestHasVoteMessageValidateBasic(t *testing.T) { + const ( + validSignedMsgType types.SignedMsgType = 0x01 + invalidSignedMsgType types.SignedMsgType = 0x03 + ) + + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageType types.SignedMsgType + messageIndex int + expectErr bool + }{ + {"Valid Message", 0, 0, validSignedMsgType, 0, false}, + {"Invalid Message", -1, 0, validSignedMsgType, 0, true}, + {"Invalid Message", 0, -1, validSignedMsgType, 0, true}, + {"Invalid Message", 0, 0, invalidSignedMsgType, 0, true}, + {"Invalid Message", 0, 0, validSignedMsgType, -1, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := HasVoteMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Type: tc.messageType, + Index: tc.messageIndex, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestVoteSetMaj23MessageValidateBasic(t *testing.T) { + const ( + validSignedMsgType types.SignedMsgType = 0x01 + invalidSignedMsgType types.SignedMsgType = 0x03 + ) + + validBlockID := types.BlockID{} + invalidBlockID := types.BlockID{ + Hash: cmn.HexBytes{}, + PartsHeader: types.PartSetHeader{ + Total: -1, + Hash: cmn.HexBytes{}, + }, + } + + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageType types.SignedMsgType + messageBlockID types.BlockID + expectErr bool + }{ + {"Valid Message", 0, 0, validSignedMsgType, validBlockID, false}, + {"Invalid Message", -1, 0, validSignedMsgType, validBlockID, true}, + {"Invalid Message", 0, -1, validSignedMsgType, validBlockID, true}, + {"Invalid Message", 0, 0, invalidSignedMsgType, validBlockID, true}, + {"Invalid Message", 0, 0, validSignedMsgType, invalidBlockID, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := VoteSetMaj23Message{ + Height: tc.messageHeight, + Round: tc.messageRound, + Type: tc.messageType, + BlockID: tc.messageBlockID, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestVoteSetBitsMessageValidateBasic(t *testing.T) { + const ( + validSignedMsgType types.SignedMsgType = 0x01 + invalidSignedMsgType types.SignedMsgType = 0x03 + ) + + validBlockID := types.BlockID{} + invalidBlockID := types.BlockID{ + Hash: cmn.HexBytes{}, + PartsHeader: types.PartSetHeader{ + Total: -1, + Hash: cmn.HexBytes{}, + }, + } + testBitArray := cmn.NewBitArray(1) + + testCases := []struct { + testName string + messageHeight int64 + messageRound int + messageType types.SignedMsgType + messageBlockID types.BlockID + messageVotes *cmn.BitArray + expectErr bool + }{ + {"Valid Message", 0, 0, validSignedMsgType, validBlockID, testBitArray, false}, + {"Invalid Message", -1, 0, validSignedMsgType, validBlockID, testBitArray, true}, + {"Invalid Message", 0, -1, validSignedMsgType, validBlockID, testBitArray, true}, + {"Invalid Message", 0, 0, invalidSignedMsgType, validBlockID, testBitArray, true}, + {"Invalid Message", 0, 0, validSignedMsgType, invalidBlockID, testBitArray, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + message := VoteSetBitsMessage{ + Height: tc.messageHeight, + Round: tc.messageRound, + Type: tc.messageType, + // Votes: tc.messageVotes, + BlockID: tc.messageBlockID, + } + + assert.Equal(t, tc.expectErr, message.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} diff --git a/types/block_test.go b/types/block_test.go index ff7edd27a..716229bbc 100644 --- a/types/block_test.go +++ b/types/block_test.go @@ -366,3 +366,91 @@ func TestCommitToVoteSet(t *testing.T) { assert.Equal(t, vote1bz, vote3bz) } } + +func TestSignedHeaderValidateBasic(t *testing.T) { + commit := randCommit() + chainID := "𠜎" + timestamp := time.Date(math.MaxInt64, 0, 0, 0, 0, 0, math.MaxInt64, time.UTC) + h := Header{ + Version: version.Consensus{Block: math.MaxInt64, App: math.MaxInt64}, + ChainID: chainID, + Height: commit.Height(), + Time: timestamp, + NumTxs: math.MaxInt64, + TotalTxs: math.MaxInt64, + LastBlockID: commit.BlockID, + LastCommitHash: commit.Hash(), + DataHash: commit.Hash(), + ValidatorsHash: commit.Hash(), + NextValidatorsHash: commit.Hash(), + ConsensusHash: commit.Hash(), + AppHash: commit.Hash(), + LastResultsHash: commit.Hash(), + EvidenceHash: commit.Hash(), + ProposerAddress: crypto.AddressHash([]byte("proposer_address")), + } + + validSignedHeader := SignedHeader{Header: &h, Commit: commit} + validSignedHeader.Commit.BlockID.Hash = validSignedHeader.Hash() + invalidSignedHeader := SignedHeader{} + + testCases := []struct { + testName string + shHeader *Header + shCommit *Commit + expectErr bool + }{ + {"Valid Signed Header", validSignedHeader.Header, validSignedHeader.Commit, false}, + {"Invalid Signed Header", invalidSignedHeader.Header, validSignedHeader.Commit, true}, + {"Invalid Signed Header", validSignedHeader.Header, invalidSignedHeader.Commit, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + sh := SignedHeader{ + Header: tc.shHeader, + Commit: tc.shCommit, + } + assert.Equal(t, tc.expectErr, sh.ValidateBasic(validSignedHeader.Header.ChainID) != nil, "Validate Basic had an unexpected result") + }) + } +} + +func TestBlockIDValidateBasic(t *testing.T) { + validBlockID := BlockID{ + Hash: cmn.HexBytes{}, + PartsHeader: PartSetHeader{ + Total: 1, + Hash: cmn.HexBytes{}, + }, + } + + invalidBlockID := BlockID{ + Hash: []byte{0}, + PartsHeader: PartSetHeader{ + Total: -1, + Hash: cmn.HexBytes{}, + }, + } + + testCases := []struct { + testName string + blockIDHash cmn.HexBytes + blockIDPartsHeader PartSetHeader + expectErr bool + }{ + {"Valid BlockID", validBlockID.Hash, validBlockID.PartsHeader, false}, + {"Invalid BlockID", invalidBlockID.Hash, validBlockID.PartsHeader, true}, + {"Invalid BlockID", validBlockID.Hash, invalidBlockID.PartsHeader, true}, + } + + for _, tc := range testCases { + t.Run(tc.testName, func(t *testing.T) { + blockID := BlockID{ + Hash: tc.blockIDHash, + PartsHeader: tc.blockIDPartsHeader, + } + assert.Equal(t, tc.expectErr, blockID.ValidateBasic() != nil, "Validate Basic had an unexpected result") + }) + } +} diff --git a/types/evidence_test.go b/types/evidence_test.go index 1f1338cad..fc97ae409 100644 --- a/types/evidence_test.go +++ b/types/evidence_test.go @@ -157,3 +157,13 @@ func TestDuplicateVoteEvidenceValidation(t *testing.T) { }) } } + +func TestMockGoodEvidenceValidateBasic(t *testing.T) { + goodEvidence := NewMockGoodEvidence(int64(1), 1, []byte{1}) + assert.Nil(t, goodEvidence.ValidateBasic()) +} + +func TestMockBadEvidenceValidateBasic(t *testing.T) { + badEvidence := MockBadEvidence{MockGoodEvidence: NewMockGoodEvidence(int64(1), 1, []byte{1})} + assert.Nil(t, badEvidence.ValidateBasic()) +} From af64a4a032eadb3a8bee68f3dbd8286ae87b29e4 Mon Sep 17 00:00:00 2001 From: Ivan Kushmantsev Date: Mon, 12 Aug 2019 14:39:11 +0400 Subject: [PATCH 174/211] docs: "Writing a Tendermint Core application in Java (gRPC)" guide (#3887) * add abci grpc java guide * fix grammar and spelling --- docs/guides/java.md | 600 ++++++++++++++++++++++++++++++++++++++++++ docs/guides/kotlin.md | 63 +++-- 2 files changed, 631 insertions(+), 32 deletions(-) create mode 100644 docs/guides/java.md diff --git a/docs/guides/java.md b/docs/guides/java.md new file mode 100644 index 000000000..162b40fd7 --- /dev/null +++ b/docs/guides/java.md @@ -0,0 +1,600 @@ +# Creating an application in Java + +## Guide Assumptions + +This guide is designed for beginners who want to get started with a Tendermint +Core application from scratch. It does not assume that you have any prior +experience with Tendermint Core. + +Tendermint Core is Byzantine Fault Tolerant (BFT) middleware that takes a state +transition machine (your application) - written in any programming language - and securely +replicates it on many machines. + +By following along with this guide, you'll create a Tendermint Core project +called kvstore, a (very) simple distributed BFT key-value store. The application (which should +implementing the blockchain interface (ABCI)) will be written in Java. + +This guide assumes that you are not new to JVM world. If you are new please see [JVM Minimal Survival Guide](https://hadihariri.com/2013/12/29/jvm-minimal-survival-guide-for-the-dotnet-developer/#java-the-language-java-the-ecosystem-java-the-jvm) and [Gradle Docs](https://docs.gradle.org/current/userguide/userguide.html). + +## Built-in app vs external app + +If you use Golang, you can run your app and Tendermint Core in the same process to get maximum performance. +[Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written this way. +Please refer to [Writing a built-in Tendermint Core application in Go](./go-built-in.md) guide for details. + +If you choose another language, like we did in this guide, you have to write a separate app, +which will communicate with Tendermint Core via a socket (UNIX or TCP) or gRPC. +This guide will show you how to build external application using RPC server. + +Having a separate application might give you better security guarantees as two +processes would be communicating via established binary protocol. Tendermint +Core will not have access to application's state. + +## 1.1 Installing Java and Gradle + +Please refer to [the Oracle's guide for installing JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html). + +Verify that you have installed Java successfully: + +```sh +$ java -version +java version "12.0.2" 2019-07-16 +Java(TM) SE Runtime Environment (build 12.0.2+10) +Java HotSpot(TM) 64-Bit Server VM (build 12.0.2+10, mixed mode, sharing) +``` + +You can choose any version of Java higher or equal to 8. +This guide is written using Java SE Development Kit 12. + +Make sure you have `$JAVA_HOME` environment variable set: + +```sh +$ echo $JAVA_HOME +/Library/Java/JavaVirtualMachines/jdk-12.0.2.jdk/Contents/Home +``` + +For Gradle installation, please refer to [their official guide](https://gradle.org/install/). + +## 1.2 Creating a new Java project + +We'll start by creating a new Gradle project. + +```sh +$ export KVSTORE_HOME=~/kvstore +$ mkdir $KVSTORE_HOME +$ cd $KVSTORE_HOME +``` + +Inside the example directory run: +```sh +gradle init --dsl groovy --package io.example --project-name example --type java-application --test-framework junit +``` +This will create a new project for you. The tree of files should look like: +```sh +$ tree +. +|-- build.gradle +|-- gradle +| `-- wrapper +| |-- gradle-wrapper.jar +| `-- gradle-wrapper.properties +|-- gradlew +|-- gradlew.bat +|-- settings.gradle +`-- src + |-- main + | |-- java + | | `-- io + | | `-- example + | | `-- App.java + | `-- resources + `-- test + |-- java + | `-- io + | `-- example + | `-- AppTest.java + `-- resources +``` + +When run, this should print "Hello world." to the standard output. + +```sh +$ ./gradlew run +> Task :run +Hello world. +``` + +## 1.3 Writing a Tendermint Core application + +Tendermint Core communicates with the application through the Application +BlockChain Interface (ABCI). All message types are defined in the [protobuf +file](https://github.com/tendermint/tendermint/blob/develop/abci/types/types.proto). +This allows Tendermint Core to run applications written in any programming +language. + +### 1.3.1 Compile .proto files + +Add the following piece to the top of the `build.gradle`: +```groovy +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.8' + } +} +``` + +Enable the protobuf plugin in the `plugins` section of the `build.gradle`: +```groovy +plugins { + id 'com.google.protobuf' version '0.8.8' +} +``` + +Add the following code to `build.gradle`: +```groovy +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:3.7.1" + } + plugins { + grpc { + artifact = 'io.grpc:protoc-gen-grpc-java:1.22.1' + } + } + generateProtoTasks { + all()*.plugins { + grpc {} + } + } +} +``` + +Now we should be ready to compile the `*.proto` files. + + +Copy the necessary `.proto` files to your project: +```sh +mkdir -p \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto + +cp $GOPATH/src/github.com/tendermint/tendermint/abci/types/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types/types.proto +cp $GOPATH/src/github.com/tendermint/tendermint/crypto/merkle/merkle.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/crypto/merkle/merkle.proto +cp $GOPATH/src/github.com/tendermint/tendermint/libs/common/types.proto \ + $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/libs/common/types.proto +cp $GOPATH/src/github.com/gogo/protobuf/gogoproto/gogo.proto \ + $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto/gogo.proto +``` + +Add these dependencies to `build.gradle`: +```groovy +dependencies { + implementation 'io.grpc:grpc-protobuf:1.22.1' + implementation 'io.grpc:grpc-netty-shaded:1.22.1' + implementation 'io.grpc:grpc-stub:1.22.1' +} +``` + +To generate all protobuf-type classes run: +```sh +./gradlew generateProto +``` +To verify that everything went smoothly, you can inspect the `build/generated/` directory: +```sh +$ tree build/generated/ +build/generated/ +|-- source +| `-- proto +| `-- main +| |-- grpc +| | `-- types +| | `-- ABCIApplicationGrpc.java +| `-- java +| |-- com +| | `-- google +| | `-- protobuf +| | `-- GoGoProtos.java +| |-- common +| | `-- Types.java +| |-- merkle +| | `-- Merkle.java +| `-- types +| `-- Types.java +``` + +### 1.3.2 Implementing ABCI + +The resulting `$KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java` file +contains the abstract class `ABCIApplicationImplBase`, which is an interface we'll need to implement. + +Create `$KVSTORE_HOME/src/main/java/io/example/KVStoreApp.java` file with the following content: +```java +package io.example; + +import io.grpc.stub.StreamObserver; +import types.ABCIApplicationGrpc; +import types.Types.*; + +class KVStoreApp extends ABCIApplicationGrpc.ABCIApplicationImplBase { + + // methods implementation + +} +``` + +Now I will go through each method of `ABCIApplicationImplBase` explaining when it's called and adding +required business logic. + +### 1.3.3 CheckTx + +When a new transaction is added to the Tendermint Core, it will ask the +application to check it (validate the format, signatures, etc.). + +```java +@Override +public void checkTx(RequestCheckTx req, StreamObserver responseObserver) { + var tx = req.getTx(); + int code = validate(tx); + var resp = ResponseCheckTx.newBuilder() + .setCode(code) + .setGasWanted(1) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} + +private int validate(ByteString tx) { + List parts = split(tx, '='); + if (parts.size() != 2) { + return 1; + } + byte[] key = parts.get(0); + byte[] value = parts.get(1); + + // check if the same key=value already exists + var stored = getPersistedValue(key); + if (stored != null && Arrays.equals(stored, value)) { + return 2; + } + + return 0; +} + +private List split(ByteString tx, char separator) { + var arr = tx.toByteArray(); + int i; + for (i = 0; i < tx.size(); i++) { + if (arr[i] == (byte)separator) { + break; + } + } + if (i == tx.size()) { + return Collections.emptyList(); + } + return List.of( + tx.substring(0, i).toByteArray(), + tx.substring(i + 1).toByteArray() + ); +} +``` + +Don't worry if this does not compile yet. + +If the transaction does not have a form of `{bytes}={bytes}`, we return `1` +code. When the same key=value already exist (same key and value), we return `2` +code. For others, we return a zero code indicating that they are valid. + +Note that anything with non-zero code will be considered invalid (`-1`, `100`, +etc.) by Tendermint Core. + +Valid transactions will eventually be committed given they are not too big and +have enough gas. To learn more about gas, check out ["the +specification"](https://tendermint.com/docs/spec/abci/apps.html#gas). + +For the underlying key-value store we'll use +[JetBrains Xodus](https://github.com/JetBrains/xodus), which is a transactional schema-less embedded high-performance database written in Java. + +`build.gradle`: +```groovy +dependencies { + implementation 'org.jetbrains.xodus:xodus-environment:1.3.91' +} +``` + +```java +... +import jetbrains.exodus.ArrayByteIterable; +import jetbrains.exodus.ByteIterable; +import jetbrains.exodus.env.Environment; +import jetbrains.exodus.env.Store; +import jetbrains.exodus.env.StoreConfig; +import jetbrains.exodus.env.Transaction; + +class KVStoreApp extends ABCIApplicationGrpc.ABCIApplicationImplBase { + private Environment env; + private Transaction txn = null; + private Store store = null; + + KVStoreApp(Environment env) { + this.env = env; + } + + ... + + private byte[] getPersistedValue(byte[] k) { + return env.computeInReadonlyTransaction(txn -> { + var store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn); + ByteIterable byteIterable = store.get(txn, new ArrayByteIterable(k)); + if (byteIterable == null) { + return null; + } + return byteIterable.getBytesUnsafe(); + }); + } +} +``` + +### 1.3.4 BeginBlock -> DeliverTx -> EndBlock -> Commit + +When Tendermint Core has decided on the block, it's transferred to the +application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and +`EndBlock` in the end. `DeliverTx` are being transferred asynchronously, but the +responses are expected to come in order. + +```java +@Override +public void beginBlock(RequestBeginBlock req, StreamObserver responseObserver) { + txn = env.beginTransaction(); + store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn); + var resp = ResponseBeginBlock.newBuilder().build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} +``` +Here we begin a new transaction, which will accumulate the block's transactions and open the corresponding store. + +```java +@Override +public void deliverTx(RequestDeliverTx req, StreamObserver responseObserver) { + var tx = req.getTx(); + int code = validate(tx); + if (code == 0) { + List parts = split(tx, '='); + var key = new ArrayByteIterable(parts.get(0)); + var value = new ArrayByteIterable(parts.get(1)); + store.put(txn, key, value); + } + var resp = ResponseDeliverTx.newBuilder() + .setCode(code) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} +``` + +If the transaction is badly formatted or the same key=value already exist, we +again return the non-zero code. Otherwise, we add it to the store. + +In the current design, a block can include incorrect transactions (those who +passed `CheckTx`, but failed `DeliverTx` or transactions included by the proposer +directly). This is done for performance reasons. + +Note we can't commit transactions inside the `DeliverTx` because in such case +`Query`, which may be called in parallel, will return inconsistent data (i.e. +it will report that some value already exist even when the actual block was not +yet committed). + +`Commit` instructs the application to persist the new state. + +```java +@Override +public void commit(RequestCommit req, StreamObserver responseObserver) { + txn.commit(); + var resp = ResponseCommit.newBuilder() + .setData(ByteString.copyFrom(new byte[8])) + .build(); + responseObserver.onNext(resp); + responseObserver.onCompleted(); +} +``` + +### 1.3.5 Query + +Now, when the client wants to know whenever a particular key/value exist, it +will call Tendermint Core RPC `/abci_query` endpoint, which in turn will call +the application's `Query` method. + +Applications are free to provide their own APIs. But by using Tendermint Core +as a proxy, clients (including [light client +package](https://godoc.org/github.com/tendermint/tendermint/lite)) can leverage +the unified API across different applications. Plus they won't have to call the +otherwise separate Tendermint Core API for additional proofs. + +Note we don't include a proof here. + +```java +@Override +public void query(RequestQuery req, StreamObserver responseObserver) { + var k = req.getData().toByteArray(); + var v = getPersistedValue(k); + var builder = ResponseQuery.newBuilder(); + if (v == null) { + builder.setLog("does not exist"); + } else { + builder.setLog("exists"); + builder.setKey(ByteString.copyFrom(k)); + builder.setValue(ByteString.copyFrom(v)); + } + responseObserver.onNext(builder.build()); + responseObserver.onCompleted(); +} +``` + +The complete specification can be found +[here](https://tendermint.com/docs/spec/abci/). + +## 1.4 Starting an application and a Tendermint Core instances + +Put the following code into the `$KVSTORE_HOME/src/main/java/io/example/App.java` file: + +```java +package io.example; + +import jetbrains.exodus.env.Environment; +import jetbrains.exodus.env.Environments; + +import java.io.IOException; + +public class App { + public static void main(String[] args) throws IOException, InterruptedException { + try (Environment env = Environments.newInstance("tmp/storage")) { + var app = new KVStoreApp(env); + var server = new GrpcServer(app, 26658); + server.start(); + server.blockUntilShutdown(); + } + } +} +``` + +It is the entry point of the application. +Here we create a special object `Environment`, which knows where to store the application state. +Then we create and start the gRPC server to handle Tendermint Core requests. + +Create the `$KVSTORE_HOME/src/main/java/io/example/GrpcServer.java` file with the following content: +```java +package io.example; + +import io.grpc.BindableService; +import io.grpc.Server; +import io.grpc.ServerBuilder; + +import java.io.IOException; + +class GrpcServer { + private Server server; + + GrpcServer(BindableService service, int port) { + this.server = ServerBuilder.forPort(port) + .addService(service) + .build(); + } + + void start() throws IOException { + server.start(); + System.out.println("gRPC server started, listening on $port"); + Runtime.getRuntime().addShutdownHook(new Thread(() -> { + System.out.println("shutting down gRPC server since JVM is shutting down"); + GrpcServer.this.stop(); + System.out.println("server shut down"); + })); + } + + private void stop() { + server.shutdown(); + } + + /** + * Await termination on the main thread since the grpc library uses daemon threads. + */ + void blockUntilShutdown() throws InterruptedException { + server.awaitTermination(); + } +} +``` + +## 1.5 Getting Up and Running + +To create a default configuration, nodeKey and private validator files, let's +execute `tendermint init`. But before we do that, we will need to install +Tendermint Core. + +```sh +$ rm -rf /tmp/example +$ cd $GOPATH/src/github.com/tendermint/tendermint +$ make install +$ TMHOME="/tmp/example" tendermint init + +I[2019-07-16|18:20:36.480] Generated private validator module=main keyFile=/tmp/example/config/priv_validator_key.json stateFile=/tmp/example2/data/priv_validator_state.json +I[2019-07-16|18:20:36.481] Generated node key module=main path=/tmp/example/config/node_key.json +I[2019-07-16|18:20:36.482] Generated genesis file module=main path=/tmp/example/config/genesis.json +``` + +Feel free to explore the generated files, which can be found at +`/tmp/example/config` directory. Documentation on the config can be found +[here](https://tendermint.com/docs/tendermint-core/configuration.html). + +We are ready to start our application: + +```sh +./gradlew run + +gRPC server started, listening on 26658 +``` + +Then we need to start Tendermint Core and point it to our application. Staying +within the application directory execute: + +```sh +$ TMHOME="/tmp/example" tendermint node --abci grpc --proxy_app tcp://127.0.0.1:26658 + +I[2019-07-28|15:44:53.632] Version info module=main software=0.32.1 block=10 p2p=7 +I[2019-07-28|15:44:53.677] Starting Node module=main impl=Node +I[2019-07-28|15:44:53.681] Started node module=main nodeInfo="{ProtocolVersion:{P2P:7 Block:10 App:0} ID_:7639e2841ccd47d5ae0f5aad3011b14049d3f452 ListenAddr:tcp://0.0.0.0:26656 Network:test-chain-Nhl3zk Version:0.32.1 Channels:4020212223303800 Moniker:Ivans-MacBook-Pro.local Other:{TxIndex:on RPCAddress:tcp://127.0.0.1:26657}}" +I[2019-07-28|15:44:54.801] Executed block module=state height=8 validTxs=0 invalidTxs=0 +I[2019-07-28|15:44:54.814] Committed state module=state height=8 txs=0 appHash=0000000000000000 +``` + +Now open another tab in your terminal and try sending a transaction: + +```sh +$ curl -s 'localhost:26657/broadcast_tx_commit?tx="tendermint=rocks"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "check_tx": { + "gasWanted": "1" + }, + "deliver_tx": {}, + "hash": "CDD3C6DFA0A08CAEDF546F9938A2EEC232209C24AA0E4201194E0AFB78A2C2BB", + "height": "33" +} +``` + +Response should contain the height where this transaction was committed. + +Now let's check if the given key now exists and its value: + +```sh +$ curl -s 'localhost:26657/abci_query?data="tendermint"' +{ + "jsonrpc": "2.0", + "id": "", + "result": { + "response": { + "log": "exists", + "key": "dGVuZGVybWludA==", + "value": "cm9ja3My" + } + } +} +``` + +`dGVuZGVybWludA==` and `cm9ja3M=` are the base64-encoding of the ASCII of `tendermint` and `rocks` accordingly. + +## Outro + +I hope everything went smoothly and your first, but hopefully not the last, +Tendermint Core application is up and running. If not, please [open an issue on +Github](https://github.com/tendermint/tendermint/issues/new/choose). To dig +deeper, read [the docs](https://tendermint.com/docs/). + +The full source code of this example project can be found [here](https://github.com/climber73/tendermint-abci-grpc-java). diff --git a/docs/guides/kotlin.md b/docs/guides/kotlin.md index 8f462bd61..fa9e10b35 100644 --- a/docs/guides/kotlin.md +++ b/docs/guides/kotlin.md @@ -22,9 +22,9 @@ If you use Golang, you can run your app and Tendermint Core in the same process [Cosmos SDK](https://github.com/cosmos/cosmos-sdk) is written this way. Please refer to [Writing a built-in Tendermint Core application in Go](./go-built-in.md) guide for details. -If you choose another language, like we did in this guide, you have to write a separate app using -either plain socket or gRPC. This guide will show you how to build external applicationg -using RPC server. +If you choose another language, like we did in this guide, you have to write a separate app, +which will communicate with Tendermint Core via a socket (UNIX or TCP) or gRPC. +This guide will show you how to build external application using RPC server. Having a separate application might give you better security guarantees as two processes would be communicating via established binary protocol. Tendermint @@ -34,7 +34,7 @@ Core will not have access to application's state. Please refer to [the Oracle's guide for installing JDK](https://www.oracle.com/technetwork/java/javase/downloads/index.html). -Verify that you have installed Java successully: +Verify that you have installed Java successfully: ```sh $ java -version @@ -69,7 +69,7 @@ Inside the example directory run: ```sh gradle init --dsl groovy --package io.example --project-name example --type kotlin-application ``` -That Gradle command will create project structure for you: +This will create a new project for you. The tree of files should look like: ```sh $ tree . @@ -114,7 +114,7 @@ language. ### 1.3.1 Compile .proto files -Add folowing to the top of `build.gradle`: +Add the following piece to the top of the `build.gradle`: ```groovy buildscript { repositories { @@ -126,14 +126,14 @@ buildscript { } ``` -Enable protobuf plugin in `plugins` section of `build.gradle`: +Enable the protobuf plugin in the `plugins` section of the `build.gradle`: ```groovy plugins { id 'com.google.protobuf' version '0.8.8' } ``` -Add following to `build.gradle`: +Add the following code to `build.gradle`: ```groovy protobuf { protoc { @@ -152,10 +152,10 @@ protobuf { } ``` -Now your project is ready to compile `*.proto` files. +Now we should be ready to compile the `*.proto` files. -Copy necessary .proto files to your project: +Copy the necessary `.proto` files to your project: ```sh mkdir -p \ $KVSTORE_HOME/src/main/proto/github.com/tendermint/tendermint/abci/types \ @@ -173,7 +173,7 @@ cp $GOPATH/src/github.com/gogo/protobuf/gogoproto/gogo.proto \ $KVSTORE_HOME/src/main/proto/github.com/gogo/protobuf/gogoproto/gogo.proto ``` -Add dependency to `build.gradle`: +Add these dependencies to `build.gradle`: ```groovy dependencies { implementation 'io.grpc:grpc-protobuf:1.22.1' @@ -186,7 +186,7 @@ To generate all protobuf-type classes run: ```sh ./gradlew generateProto ``` -It will produce java classes to `build/generated/`: +To verify that everything went smoothly, you can inspect the `build/generated/` directory: ```sh $ tree build/generated/ build/generated/ @@ -211,11 +211,10 @@ build/generated/ ### 1.3.2 Implementing ABCI -As you can see there is a generated file `$KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java`. -which contains an abstract class `ABCIApplicationImplBase`. This class fully describes the ABCI interface. -All you need is implement this interface. +The resulting `$KVSTORE_HOME/build/generated/source/proto/main/grpc/types/ABCIApplicationGrpc.java` file +contains the abstract class `ABCIApplicationImplBase`, which is an interface we'll need to implement. -Create file `$KVSTORE_HOME/src/main/kotlin/io/example/KVStoreApp.kt` with following context: +Create `$KVSTORE_HOME/src/main/kotlin/io/example/KVStoreApp.kt` file with the following content: ```kotlin package io.example @@ -296,7 +295,7 @@ For the underlying key-value store we'll use `build.gradle`: ```groovy dependencies { - implementation "org.jetbrains.xodus:xodus-environment:1.3.91" + implementation 'org.jetbrains.xodus:xodus-environment:1.3.91' } ``` @@ -316,14 +315,21 @@ class KVStoreApp( private var store: Store? = null ... + + private fun getPersistedValue(k: ByteArray): ByteArray? { + return env.computeInReadonlyTransaction { txn -> + val store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn) + store.get(txn, ArrayByteIterable(k))?.bytesUnsafe + } + } } ``` ### 1.3.4 BeginBlock -> DeliverTx -> EndBlock -> Commit -When Tendermint Core has decided on the block, it's transfered to the +When Tendermint Core has decided on the block, it's transferred to the application in 3 parts: `BeginBlock`, one `DeliverTx` per transaction and -`EndBlock` in the end. DeliverTx are being transfered asynchronously, but the +`EndBlock` in the end. `DeliverTx` are being transferred asynchronously, but the responses are expected to come in order. ```kotlin @@ -335,7 +341,7 @@ override fun beginBlock(req: RequestBeginBlock, responseObserver: StreamObserver responseObserver.onCompleted() } ``` -Here we start new transaction, which will store block's transactions, and open corresponding store. +Here we begin a new transaction, which will accumulate the block's transactions and open the corresponding store. ```kotlin override fun deliverTx(req: RequestDeliverTx, responseObserver: StreamObserver) { @@ -355,10 +361,10 @@ override fun deliverTx(req: RequestDeliverTx, responseObserver: StreamObserver - val store = env.openStore("store", StoreConfig.WITHOUT_DUPLICATES, txn) - store.get(txn, ArrayByteIterable(k))?.bytesUnsafe - } -} ``` The complete specification can be found @@ -440,10 +439,10 @@ fun main() { ``` It is the entry point of the application. -Here we create special object `Environment` which knows where to store state of the application. -Then we create and srart gRPC server to handle Tendermint's requests. +Here we create a special object `Environment`, which knows where to store the application state. +Then we create and start the gRPC server to handle Tendermint Core requests. -Create file `$KVSTORE_HOME/src/main/kotlin/io/example/GrpcServer.kt`: +Create `$KVSTORE_HOME/src/main/kotlin/io/example/GrpcServer.kt` file with the following content: ```kotlin package io.example From 502c21013fc4540cc5b3762202a798b00f3a631d Mon Sep 17 00:00:00 2001 From: HaoyangLiu Date: Tue, 10 Sep 2019 20:56:11 +0800 Subject: [PATCH 175/211] remove tm-db dependency --- go.mod | 6 +-- go.sum | 71 ++++++++++++++++++++++--------- state/txindex/kv/kv_bench_test.go | 2 +- 3 files changed, 55 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 5e37b6c59..ae5fa8e21 100644 --- a/go.mod +++ b/go.mod @@ -16,10 +16,8 @@ require ( github.com/golang/protobuf v1.3.2 github.com/golang/snappy v0.0.1 github.com/google/gofuzz v1.0.0 // indirect - github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70 // indirect github.com/gorilla/websocket v1.2.0 github.com/hashicorp/hcl v1.0.0 // indirect - github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect github.com/jmhodges/levigo v1.0.0 github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 // indirect @@ -44,9 +42,9 @@ require ( github.com/stretchr/testify v1.3.0 github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 github.com/tendermint/go-amino v0.14.1 - github.com/tendermint/tm-db v0.1.1 - golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c // indirect + go.etcd.io/bbolt v1.3.3 // indirect golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 golang.org/x/net v0.0.0-20190909003024-a7b16738d86b + google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 // indirect google.golang.org/grpc v1.23.0 ) diff --git a/go.sum b/go.sum index d29c060fd..8924eebe3 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,15 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d h1:xG8Pj6Y6J760xwETNmMzmlt38QSwz0BLp1cZ09g27uw= github.com/btcsuite/btcd v0.0.0-20190115013929-ed77733ec07d/go.mod h1:d3C0AkH6BRcvO8T0UEPu53cnw4IbV63x1bEjildYhO0= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a h1:RQMUrEILyYJEoAT34XS/kLu40vC0+po/UfxrBBA4qZE= github.com/btcsuite/btcutil v0.0.0-20180706230648-ab6388e0c60a/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd/go.mod h1:HHNXQzUsZCxOoE+CPiyCTO6x34Zs86zZUiwtpXoGdtg= github.com/btcsuite/goleveldb v0.0.0-20160330041536-7834afc9e8cd/go.mod h1:F+uVaaLLH7j4eDXPRvw78tMflu7Ie2bzYOH4Y8rRKBY= @@ -14,88 +19,117 @@ github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46f github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/etcd-io/bbolt v1.3.2/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/etcd-io/bbolt v1.3.3 h1:gSJmxrs37LgTqR/oyJBWok6k6SvXEUerFTbltIhXkBM= github.com/etcd-io/bbolt v1.3.3/go.mod h1:ZF2nL25h33cCyBtcyWeZ2/I3HQOfTP+0PIEvHjkjCrw= +github.com/fortytw2/leaktest v1.2.0 h1:cj6GCiwJDH7l3tMHLjZDo0QqPtrXJiWSI9JgpeQKw+Q= github.com/fortytw2/leaktest v1.2.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-kit/kit v0.6.0 h1:wTifptAGIyIuir4bRyN4h7+kAa2a4eepLYVmRe5qqQ8= github.com/go-kit/kit v0.6.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0 h1:8HUsc87TaSWLKwrnumgC8/YconD2fJQsRJAsWaPg2ic= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE= github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.0/go.mod h1:Qd/q+1AKNOZr9uGQzbzCmRO6sUih6GTPZv6a1/R87v0= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70 h1:XTnP8fJpa4Kvpw2qARB4KS9izqxPS0Sd92cDlY3uk+w= -github.com/google/pprof v0.0.0-20190723021845-34ac40c74b70/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ= github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6 h1:UDMh68UUwekSh5iP2OMhRRZJiiBccgV7axzUG8vi56c= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U= github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ= github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515 h1:T+h1c/A9Gawja4Y9mFVWj2vyii2bbUNDw3kt9VxK2EY= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/libp2p/go-buffer-pool v0.0.2 h1:QNK2iAFa8gjAe1SPz6mHSMuCcjs+X1wlHzeOSqcmlfs= github.com/libp2p/go-buffer-pool v0.0.2/go.mod h1:MvaB6xw5vOrDl8rYZGLFdKAuk/hRoRZd1Vi32+RXyFM= +github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1 h1:K47Rk0v/fkEfwfQet2KWhscE0cJzjgCCDBG2KHZoVno= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910 h1:idejC8f05m9MGOsuEi1ATq9shN03HrxNkD/luQvxCv8= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39 h1:Cto4X6SVMWRPBkJ/3YHn1iDGDGc/Z+sW+AEMKHMVvN4= github.com/prometheus/common v0.0.0-20181020173914-7e9e6cabbd39/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d h1:GoAlyOgbOEIFdaDqxJVlbOQ1DtGmZWs/Qau0hIlk+WQ= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165 h1:nkcn14uNmFEuGCb2mBZbBb24RdNRL08b/wb+xBOYpuk= github.com/rcrowley/go-metrics v0.0.0-20180503174638-e2704e165165/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rs/cors v1.6.0 h1:G9tHG9lebljV9mfp9SNPDL36nCDxmo3zTlAf1YgvzmI= github.com/rs/cors v1.6.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= +github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cobra v0.0.1 h1:zZh3X5aZbdnoj+4XkaBxKfhO4ot82icYdhhREIAXIj8= github.com/spf13/cobra v0.0.1/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= +github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/viper v1.0.0 h1:RUA/ghS2i64rlnn4ydTfblY8Og8QzcPtCcHvgMn+w/I= github.com/spf13/viper v1.0.0/go.mod h1:A8kyI5cUJhb8N+3pkfONlcEcZbueH6nhAm0Fq7SrnBM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965 h1:1oFLiOyVl+W7bnBzGhf7BbIv9loSFQcieWWYIjLqcAw= github.com/syndtr/goleveldb v1.0.1-0.20190318030020-c3a204f8e965/go.mod h1:9OrXJhf154huy1nPWmuSrkgjPUtUNhA+Zmy+6AESzuA= +github.com/tendermint/go-amino v0.14.1 h1:o2WudxNfdLNBwMyl2dqOJxiro5rfrEaU0Ugs6offJMk= github.com/tendermint/go-amino v0.14.1/go.mod h1:i/UKE5Uocn+argJJBb12qTZsCDBcAYMbR92AaJVmKso= -github.com/tendermint/tm-cmn v0.1.1 h1:sSbm79PeQQdmbgiPjFDRT70cSXBVFVmxn5/sQrCNrUI= -github.com/tendermint/tm-db v0.1.1/go.mod h1:0cPKWu2Mou3IlxecH+MEUSYc1Ch537alLe6CpFrKzgw= +go.etcd.io/bbolt v1.3.3 h1:MUGmc65QhB3pIlaQ5bB4LwqSj6GIonVJXpZiaKNyaKk= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= -golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c h1:Rx/HTKi09myZ25t1SOlDHmHOy/mKxNAcu0hP1oPX9qM= -golang.org/x/arch v0.0.0-20190312162104-788fe5ffcd8c/go.mod h1:flIaEI6LNU6xOCD5PaJvn9wGP0agmIOqjrtsKGRguv4= golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25 h1:jsG6UpNLt9iAsb0S2AGW28DveNzzgmbXR+ENoPjUeIU= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190909003024-a7b16738d86b h1:XfVGCX+0T4WOStkaOsJRllbsiImhB2jgVBGc9L0lPGc= golang.org/x/net v0.0.0-20190909003024-a7b16738d86b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -103,19 +137,18 @@ golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8 h1:Nw54tB0rB7hY/N0NQvRW8DG4Yk3Q6T9cu9RcFQDu1tc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2 h1:67iHsV9djwGdZpdZNbLuQj6FOzCaZe3w+vhLjn5AcFA= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/grpc v1.13.0 h1:bHIbVsCwmvbArgCJmLdgOdHFXlKqTOVjbibbS19cXHc= -google.golang.org/grpc v1.13.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= -google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0 h1:AzbTB6ux+okLTzP8Ru1Xs41C303zdcfEht7MQnYJt5A= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/state/txindex/kv/kv_bench_test.go b/state/txindex/kv/kv_bench_test.go index 9c3442a01..eeb716f08 100644 --- a/state/txindex/kv/kv_bench_test.go +++ b/state/txindex/kv/kv_bench_test.go @@ -8,9 +8,9 @@ import ( abci "github.com/tendermint/tendermint/abci/types" cmn "github.com/tendermint/tendermint/libs/common" + dbm "github.com/tendermint/tendermint/libs/db" "github.com/tendermint/tendermint/libs/pubsub/query" "github.com/tendermint/tendermint/types" - dbm "github.com/tendermint/tm-db" ) func BenchmarkTxSearch(b *testing.B) { From 73d18ded74ddfa9c426989a31f9796fec1189fe7 Mon Sep 17 00:00:00 2001 From: Anton Kaliaev Date: Tue, 13 Aug 2019 12:32:40 +0400 Subject: [PATCH 176/211] store: register block amino, not just crypto (#3894) * store: register block amino, not just crypto Fixes #3893 * update changelog --- CHANGELOG_PENDING.md | 1 + store/codec.go | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 1f745e3c7..9bd5977ab 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -20,3 +20,4 @@ program](https://hackerone.com/tendermint). ### BUG FIXES: - [config] \#3868 move misplaced `max_msg_bytes` into mempool section +- [store] \#3893 register block amino, not just crypto diff --git a/store/codec.go b/store/codec.go index 67b838c01..4895e8994 100644 --- a/store/codec.go +++ b/store/codec.go @@ -2,11 +2,11 @@ package store import ( amino "github.com/tendermint/go-amino" - cryptoAmino "github.com/tendermint/tendermint/crypto/encoding/amino" + "github.com/tendermint/tendermint/types" ) var cdc = amino.NewCodec() func init() { - cryptoAmino.RegisterAmino(cdc) + types.RegisterBlockAmino(cdc) } From a1297712e17b5a89488edf1f99ad7f504791f144 Mon Sep 17 00:00:00 2001 From: Zaki Manian Date: Thu, 15 Aug 2019 02:50:51 -0700 Subject: [PATCH 177/211] [ADR -44] Lite Client with Weak Subjectivity (#3795) * Initiat commit of lite client with with weak subjectivity ADR * Apply suggestions from code review Co-Authored-By: Marko * Update docs/architecture/adr-044-lite-client-with-weak-subjectivity.md Co-Authored-By: Christopher Goes * Apply suggestions from code review Co-Authored-By: Christopher Goes * Apply suggestions from code review Co-Authored-By: Anca Zamfir * fix typo and format the code block * address cwgoes comments * add Positive/Negative points * change status to accepted * Apply suggestions from code review Co-Authored-By: Christopher Goes * link the spec and explain headers caching --- ...-044-lite-client-with-weak-subjectivity.md | 141 ++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 docs/architecture/adr-044-lite-client-with-weak-subjectivity.md diff --git a/docs/architecture/adr-044-lite-client-with-weak-subjectivity.md b/docs/architecture/adr-044-lite-client-with-weak-subjectivity.md new file mode 100644 index 000000000..066f68f7f --- /dev/null +++ b/docs/architecture/adr-044-lite-client-with-weak-subjectivity.md @@ -0,0 +1,141 @@ +# ADR 044: Lite Client with Weak Subjectivity + +## Changelog +* 13-07-2019: Initial draft +* 14-08-2019: Address cwgoes comments + +## Context + +The concept of light clients was introduced in the Bitcoin white paper. It +describes a watcher of distributed consensus process that only validates the +consensus algorithm and not the state machine transactions within. + +Tendermint light clients allow bandwidth & compute-constrained devices, such as smartphones, low-power embedded chips, or other blockchains to +efficiently verify the consensus of a Tendermint blockchain. This forms the +basis of safe and efficient state synchronization for new network nodes and +inter-blockchain communication (where a light client of one Tendermint instance +runs in another chain's state machine). + +In a network that is expected to reliably punish validators for misbehavior +by slashing bonded stake and where the validator set changes +infrequently, clients can take advantage of this assumption to safely +synchronize a lite client without downloading the intervening headers. + +Light clients (and full nodes) operating in the Proof Of Stake context need a +trusted block height from a trusted source that is no older than 1 unbonding +window plus a configurable evidence submission synchrony bound. This is called “weak subjectivity”. + +Weak subjectivity is required in Proof of Stake blockchains because it is +costless for an attacker to buy up voting keys that are no longer bonded and +fork the network at some point in its prior history. See Vitalik’s post at +[Proof of Stake: How I Learned to Love Weak +Subjectivity](https://blog.ethereum.org/2014/11/25/proof-stake-learned-love-weak-subjectivity/). + +Currently, Tendermint provides a lite client implementation in the +[lite](https://github.com/tendermint/tendermint/tree/master/lite) package. This +lite client implements a bisection algorithm that tries to use a binary search +to find the minimum number of block headers where the validator set voting +power changes are less than < 1/3rd. This interface does not support weak +subjectivity at this time. The Cosmos SDK also does not support counterfactual +slashing, nor does the lite client have any capacity to report evidence making +these systems *theoretically unsafe*. + +NOTE: Tendermint provides a somewhat different (stronger) light client model +than Bitcoin under eclipse, since the eclipsing node(s) can only fool the light +client if they have two-thirds of the private keys from the last root-of-trust. + +## Decision + +### The Weak Subjectivity Interface + +Add the weak subjectivity interface for when a new light client connects to the +network or when a light client that has been offline for longer than the +unbonding period connects to the network. Specifically, the node needs to +initialize the following structure before syncing from user input: + +``` +type TrustOptions struct { + // Required: only trust commits up to this old. + // Should be equal to the unbonding period minus some delta for evidence reporting. + TrustPeriod time.Duration `json:"trust-period"` + + // Option 1: TrustHeight and TrustHash can both be provided + // to force the trusting of a particular height and hash. + // If the latest trusted height/hash is more recent, then this option is + // ignored. + TrustHeight int64 `json:"trust-height"` + TrustHash []byte `json:"trust-hash"` + + // Option 2: Callback can be set to implement a confirmation + // step if the trust store is uninitialized, or expired. + Callback func(height int64, hash []byte) error +} +``` + +The expectation is the user will get this information from a trusted source +like a validator, a friend, or a secure website. A more user friendly +solution with trust tradeoffs is that we establish an https based protocol with +a default end point that populates this information. Also an on-chain registry +of roots-of-trust (e.g. on the Cosmos Hub) seems likely in the future. + +### Linear Verification + +The linear verification algorithm requires downloading all headers +between the `TrustHeight` and the `LatestHeight`. The lite client downloads the +full header for the provided `TrustHeight` and then proceeds to download `N+1` +headers and applies the [Tendermint validation +rules](https://github.com/tendermint/tendermint/blob/master/docs/spec/blockchain/blockchain.md#validation) +to each block. + +### Bisecting Verification + +Bisecting Verification is a more bandwidth and compute intensive mechanism that +in the most optimistic case requires a light client to only download two block +headers to come into synchronization. + +The bisection algorithm proceeds in the following fashion. The client downloads +and verifies the full block header for `TrustHeight` and then fetches +`LatestHeight` blocker header. The client then verifies the `LatestHeight` +header. Finally the client attempts to verify the `LatestHeight` header with +voting powers taken from `NextValidatorSet` in the `TrustHeight` header. This +verification will succeed if the validators from `TrustHeight` still have > 2/3 ++1 of voting power in the `LatestHeight`. If this succeeds, the client is fully +synchronized. If this fails, then following Bisection Algorithm should be +executed. + +The Client tries to download the block at the mid-point block between +`LatestHeight` and `TrustHeight` and attempts that same algorithm as above +using `MidPointHeight` instead of `LatestHeight` and a different threshold - +1/3 +1 of voting power for *non-adjacent headers*. In the case the of failure, +recursively perform the `MidPoint` verification until success then start over +with an updated `NextValidatorSet` and `TrustHeight`. + +If the client encounters a forged header, it should submit the header along +with some other intermediate headers as the evidence of misbehavior to other +full nodes. After that, it can retry the bisection using another full node. An +optimal client will cache trusted headers from the previous run to minimize +network usage. + +--- + +Check out the formal specification +[here](https://github.com/tendermint/tendermint/blob/master/docs/spec/consensus/light-client.md). + +## Status + +Accepted. + +## Consequences + +### Positive + +* light client which is safe to use (it can go offline, but not for too long) + +### Negative + +* complexity of bisection + +### Neutral + +* social consensus can be prone to errors (for cases where a new light client + joins a network or it has been offline for too long) From 5afe03f69ff90386dce345ae53a7436b8950f619 Mon Sep 17 00:00:00 2001 From: Alessio Treglia Date: Fri, 16 Aug 2019 11:01:47 +0200 Subject: [PATCH 178/211] gitian: update reproducible builds to build with Go 1.12.8 (#3902) --- scripts/gitian-build.sh | 2 +- scripts/gitian-descriptors/gitian-darwin.yml | 4 ++-- scripts/gitian-descriptors/gitian-linux.yml | 4 ++-- scripts/gitian-descriptors/gitian-windows.yml | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/scripts/gitian-build.sh b/scripts/gitian-build.sh index a7a6acec3..fbe475015 100755 --- a/scripts/gitian-build.sh +++ b/scripts/gitian-build.sh @@ -8,7 +8,7 @@ set -euo pipefail GITIAN_CACHE_DIRNAME='.gitian-builder-cache' -GO_DEBIAN_RELEASE='1.12.5-1' +GO_DEBIAN_RELEASE='1.12.8-1' GO_TARBALL="golang-debian-${GO_DEBIAN_RELEASE}.tar.gz" GO_TARBALL_URL="https://salsa.debian.org/go-team/compiler/golang/-/archive/debian/${GO_DEBIAN_RELEASE}/${GO_TARBALL}" diff --git a/scripts/gitian-descriptors/gitian-darwin.yml b/scripts/gitian-descriptors/gitian-darwin.yml index 03ba1f1a4..58b4f0cb8 100644 --- a/scripts/gitian-descriptors/gitian-darwin.yml +++ b/scripts/gitian-descriptors/gitian-darwin.yml @@ -23,11 +23,11 @@ remotes: - "url": "https://github.com/tendermint/tendermint.git" "dir": "tendermint" files: -- "golang-debian-1.12.5-1.tar.gz" +- "golang-debian-1.12.8-1.tar.gz" script: | set -e -o pipefail - GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_RELEASE=golang-debian-1.12.8-1 GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" # Compile go and configure the environment export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" diff --git a/scripts/gitian-descriptors/gitian-linux.yml b/scripts/gitian-descriptors/gitian-linux.yml index f1c31c40e..6969d41d7 100644 --- a/scripts/gitian-descriptors/gitian-linux.yml +++ b/scripts/gitian-descriptors/gitian-linux.yml @@ -23,11 +23,11 @@ remotes: - "url": "https://github.com/tendermint/tendermint.git" "dir": "tendermint" files: -- "golang-debian-1.12.5-1.tar.gz" +- "golang-debian-1.12.8-1.tar.gz" script: | set -e -o pipefail - GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_RELEASE=golang-debian-1.12.8-1 GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" # Compile go and configure the environment export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" diff --git a/scripts/gitian-descriptors/gitian-windows.yml b/scripts/gitian-descriptors/gitian-windows.yml index 80b2e60d3..3215e7814 100644 --- a/scripts/gitian-descriptors/gitian-windows.yml +++ b/scripts/gitian-descriptors/gitian-windows.yml @@ -23,11 +23,11 @@ remotes: - "url": "https://github.com/tendermint/tendermint.git" "dir": "tendermint" files: -- "golang-debian-1.12.5-1.tar.gz" +- "golang-debian-1.12.8-1.tar.gz" script: | set -e -o pipefail - GO_SRC_RELEASE=golang-debian-1.12.5-1 + GO_SRC_RELEASE=golang-debian-1.12.8-1 GO_SRC_TARBALL="${GO_SRC_RELEASE}.tar.gz" # Compile go and configure the environment export TAR_OPTIONS="--mtime="$REFERENCE_DATE\\\ $REFERENCE_TIME"" From 867f966dd96f7173888737ed041159f366793b1f Mon Sep 17 00:00:00 2001 From: Karoly Albert Szabo Date: Fri, 16 Aug 2019 11:29:02 +0200 Subject: [PATCH 179/211] [RPC] Static swagger (#3880) * manually swagging Signed-off-by: Karoly Albert Szabo * three definitions with polymorphism Signed-off-by: Karoly Albert Szabo * added blockchain and block Signed-off-by: Karoly Albert Szabo * low quality generation, commit, block_response and validators Signed-off-by: Karoly Albert Szabo * genesis and consensus states endpoints Signed-off-by: Karoly Albert Szabo * fix indentation Signed-off-by: Karoly Albert Szabo * consensus parameters Signed-off-by: Karoly Albert Szabo * fix indentation Signed-off-by: Karoly Albert Szabo * add height to consensus parameters endpoint Signed-off-by: Karoly Albert Szabo * unconfirmed_txs and num_unconfirmed_txs Signed-off-by: Karoly Albert Szabo * add missing query parameter Signed-off-by: Karoly Albert Szabo * add ABCI queries Signed-off-by: Karoly Albert Szabo * added index document for swagger documentation Signed-off-by: Karoly Albert Szabo * add missing routes Signed-off-by: Karoly Albert Szabo * contract tests added on CCI Signed-off-by: Karoly Albert Szabo * contract tests job should be in the test suite Signed-off-by: Karoly Albert Szabo * simplify requirements to test Signed-off-by: Karoly Albert Szabo * typo Signed-off-by: Karoly Albert Szabo * build is a prerequisite to start localnet Signed-off-by: Karoly Albert Szabo * reduce nodejs size, move goodman to get_tools, add docs, fix comments Signed-off-by: Karoly Albert Szabo * Update scripts/get_tools.sh That's cleaner, thanks! Co-Authored-By: Anton Kaliaev * xz not supported by cci image, let's keep it simple Signed-off-by: Karoly Albert Szabo * REMOVE-indirect debug of CCI paths Signed-off-by: Karoly Albert Szabo * dirty experiment, volume is empty but binary has been produced Signed-off-by: Karoly Albert Szabo * dirty experiment, volume is empty but binary has been produced Signed-off-by: Karoly Albert Szabo * dirty experiment going on Signed-off-by: Karoly Albert Szabo * locally works, CCI have difficulties with second layaer containers volumes Signed-off-by: Karoly Albert Szabo * restore experiment, use machine instead of docker for contract tests Signed-off-by: Karoly Albert Szabo * simplify a bit Signed-off-by: Karoly Albert Szabo * rollback on machine golang Signed-off-by: Karoly Albert Szabo * Document the changes Signed-off-by: Karoly Albert Szabo * Changelog Signed-off-by: Karoly Albert Szabo * comments Signed-off-by: Karoly Albert Szabo --- .circleci/config.yml | 33 + CHANGELOG_PENDING.md | 1 + CONTRIBUTING.md | 13 + Makefile | 18 +- cmd/contract_tests/main.go | 34 + docs/spec/rpc/index.html | 25 + docs/spec/rpc/swagger.yaml | 2680 ++++++++++++++++++++++++++++++++++++ dredd.yml | 33 + go.mod | 1 + go.sum | 2 + scripts/get_nodejs.sh | 14 + scripts/get_tools.sh | 1 + 12 files changed, 2854 insertions(+), 1 deletion(-) create mode 100644 cmd/contract_tests/main.go create mode 100644 docs/spec/rpc/index.html create mode 100644 docs/spec/rpc/swagger.yaml create mode 100644 dredd.yml create mode 100755 scripts/get_nodejs.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 539dd7ee4..d350d8b7c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -359,6 +359,35 @@ jobs: - store_artifacts: path: /go/src/github.com/tendermint/tendermint/tendermint-*.tar.gz + # Test RPC implementation against the swagger documented specs + contract_tests: + working_directory: /home/circleci/.go_workspace/src/github.com/tendermint/tendermint + machine: + image: circleci/classic:latest + environment: + GOBIN: /home/circleci/.go_workspace/bin + GOPATH: /home/circleci/.go_workspace/ + GOOS: linux + GOARCH: amd64 + parallelism: 1 + steps: + - checkout + - run: + name: Test RPC endpoints agsainst swagger documentation + command: | + set -x + export PATH=~/.local/bin:$PATH + + # install node 11.15 and dredd + ./scripts/get_nodejs.sh + + # build the binaries with a proper version of Go + docker run --rm -v "$PWD":/go/src/github.com/tendermint/tendermint -w /go/src/github.com/tendermint/tendermint golang make build-linux build-contract-tests-hooks + + # This docker image works with go 1.7, we can install here the hook handler that contract-tests is going to use + go get github.com/snikch/goodman/cmd/goodman + make contract-tests + workflows: version: 2 test-suite: @@ -397,6 +426,10 @@ workflows: only: - master - /v[0-9]+\.[0-9]+/ + - contract_tests: + requires: + - setup_dependencies + release: jobs: - prepare_build diff --git a/CHANGELOG_PENDING.md b/CHANGELOG_PENDING.md index 9bd5977ab..fd0ea50e2 100644 --- a/CHANGELOG_PENDING.md +++ b/CHANGELOG_PENDING.md @@ -16,6 +16,7 @@ program](https://hackerone.com/tendermint). - [privval] \#3370 Refactors and simplifies validator/kms connection handling. Please refer to thttps://github.com/tendermint/tendermint/pull/3370#issue-257360971 - [consensus] \#3839 Reduce "Error attempting to add vote" message severity (Error -> Info) - [mempool] \#3877 Make `max_tx_bytes` configurable instead of `max_msg_bytes` +- [rpc] \#3880 Document endpoints with `swagger`, introduce contract tests of implementation against documentation ### BUG FIXES: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 832156bda..63c4de646 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -183,3 +183,16 @@ If they have `.go` files in the root directory, they will be automatically tested by circle using `go test -v -race ./...`. If not, they will need a `circle.yml`. Ideally, every repo has a `Makefile` that defines `make test` and includes its continuous integration status using a badge in the `README.md`. + +### RPC Testing + +If you contribute to the RPC endpoints it's important to document your changes in the [Swagger file](./docs/spec/rpc/swagger.yaml) +To test your changes you should install `nodejs` version `v11.15.*` and run: + +```bash +npm i -g dredd +make build-linux build-contract-tests-hooks +make contract-tests +``` + +This command will popup a network and check every endpoint against what has been documented diff --git a/Makefile b/Makefile index 1705d42ba..a5681cf34 100644 --- a/Makefile +++ b/Makefile @@ -308,7 +308,23 @@ sentry-stop: build-slate: bash scripts/slate.sh +# Build hooks for dredd, to skip or add information on some steps +build-contract-tests-hooks: +ifeq ($(OS),Windows_NT) + go build -mod=readonly $(BUILD_FLAGS) -o build/contract_tests.exe ./cmd/contract_tests +else + go build -mod=readonly $(BUILD_FLAGS) -o build/contract_tests ./cmd/contract_tests +endif + +# Run a nodejs tool to test endpoints against a localnet +# The command takes care of starting and stopping the network +# prerequisits: build-contract-tests-hooks build-linux +# the two build commands were not added to let this command run from generic containers or machines. +# The binaries should be built beforehand +contract-tests: + dredd + # To avoid unintended conflicts with file names, always add to .PHONY # unless there is a reason not to. # https://www.gnu.org/software/make/manual/html_node/Phony-Targets.html -.PHONY: check build build_race build_abci dist install install_abci check_tools get_tools update_tools draw_deps get_protoc protoc_abci protoc_libs gen_certs clean_certs grpc_dbserver test_cover test_apps test_persistence test_p2p test test_race test_integrations test_release test100 vagrant_test fmt rpc-docs build-linux localnet-start localnet-stop build-docker build-docker-localnode sentry-start sentry-config sentry-stop build-slate protoc_grpc protoc_all build_c install_c test_with_deadlock cleanup_after_test_with_deadlock lint +.PHONY: check build build_race build_abci dist install install_abci check_tools get_tools update_tools draw_deps get_protoc protoc_abci protoc_libs gen_certs clean_certs grpc_dbserver test_cover test_apps test_persistence test_p2p test test_race test_integrations test_release test100 vagrant_test fmt rpc-docs build-linux localnet-start localnet-stop build-docker build-docker-localnode sentry-start sentry-config sentry-stop build-slate protoc_grpc protoc_all build_c install_c test_with_deadlock cleanup_after_test_with_deadlock lint build-contract-tests-hooks contract-tests diff --git a/cmd/contract_tests/main.go b/cmd/contract_tests/main.go new file mode 100644 index 000000000..487537824 --- /dev/null +++ b/cmd/contract_tests/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "fmt" + "strings" + + "github.com/snikch/goodman/hooks" + "github.com/snikch/goodman/transaction" +) + +func main() { + // This must be compiled beforehand and given to dredd as parameter, in the meantime the server should be running + h := hooks.NewHooks() + server := hooks.NewServer(hooks.NewHooksRunner(h)) + h.BeforeAll(func(t []*transaction.Transaction) { + fmt.Println(t[0].Name) + }) + h.BeforeEach(func(t *transaction.Transaction) { + if strings.HasPrefix(t.Name, "Tx") || + // We need a proper example of evidence to broadcast + strings.HasPrefix(t.Name, "Info > /broadcast_evidence") || + // We need a proper example of path and data + strings.HasPrefix(t.Name, "ABCI > /abci_query") || + // We need to find a way to make a transaction before starting the tests, + // that hash should replace the dummy one in hte swagger file + strings.HasPrefix(t.Name, "Info > /tx") { + t.Skip = true + fmt.Printf("%s Has been skipped\n", t.Name) + } + }) + server.Serve() + defer server.Listener.Close() + fmt.Print("FINE") +} diff --git a/docs/spec/rpc/index.html b/docs/spec/rpc/index.html new file mode 100644 index 000000000..d6b0fc5a9 --- /dev/null +++ b/docs/spec/rpc/index.html @@ -0,0 +1,25 @@ + + + + + + Tendermint RPC + + + + + + +