From 306e96ab12115b8667152b37d0db085885899213 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 4 Jan 2024 21:35:43 +0100 Subject: [PATCH 01/26] Start client cleanup --- .../bft/rpc/client/{httpclient.go => base.go} | 125 +----------------- tm2/pkg/bft/rpc/client/http.go | 114 ++++++++++++++++ tm2/pkg/bft/rpc/client/interface.go | 1 - .../rpc/client/{localclient.go => local.go} | 0 tm2/pkg/bft/rpc/client/ws.go | 19 +++ tm2/pkg/bft/rpc/lib/client/http_client.go | 8 +- 6 files changed, 138 insertions(+), 129 deletions(-) rename tm2/pkg/bft/rpc/client/{httpclient.go => base.go} (63%) create mode 100644 tm2/pkg/bft/rpc/client/http.go rename tm2/pkg/bft/rpc/client/{localclient.go => local.go} (100%) create mode 100644 tm2/pkg/bft/rpc/client/ws.go diff --git a/tm2/pkg/bft/rpc/client/httpclient.go b/tm2/pkg/bft/rpc/client/base.go similarity index 63% rename from tm2/pkg/bft/rpc/client/httpclient.go rename to tm2/pkg/bft/rpc/client/base.go index c7295e327ed..939d81362a2 100644 --- a/tm2/pkg/bft/rpc/client/httpclient.go +++ b/tm2/pkg/bft/rpc/client/base.go @@ -1,135 +1,12 @@ package client import ( - "net/http" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/errors" ) -/* -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 -(Local), or when you want to mock out the server for test code (mock). - -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 -} - -// 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 - MempoolClient -} - -// 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) - _ rpcClient = (*BatchHTTP)(nil) - _ rpcClient = (*baseRPCClient)(nil) -) - -// ----------------------------------------------------------------------------- -// HTTP - -// NewHTTP takes a remote endpoint in the form ://: and -// the websocket path (which always seems to be "/websocket") -// The function panics if the provided remote is invalid. -func NewHTTP(remote, wsEndpoint string) *HTTP { - httpClient := rpcclient.DefaultHTTPClient(remote) - return NewHTTPWithClient(remote, wsEndpoint, httpClient) -} - -// NewHTTPWithClient allows for setting a custom http client. See NewHTTP -// The function panics if the provided client is nil or remote is invalid. -func NewHTTPWithClient(remote, wsEndpoint string, client *http.Client) *HTTP { - if client == nil { - panic("nil http.Client provided") - } - rc := rpcclient.NewJSONRPCClientWithHTTPClient(remote, client) - - return &HTTP{ - rpc: rc, - remote: remote, - baseRPCClient: &baseRPCClient{caller: rc}, - } -} - -var _ Client = (*HTTP)(nil) - -// 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 +var _ Client = (*baseRPCClient)(nil) func (c *baseRPCClient) Status() (*ctypes.ResultStatus, error) { result := new(ctypes.ResultStatus) diff --git a/tm2/pkg/bft/rpc/client/http.go b/tm2/pkg/bft/rpc/client/http.go new file mode 100644 index 00000000000..4e73cb8f7c7 --- /dev/null +++ b/tm2/pkg/bft/rpc/client/http.go @@ -0,0 +1,114 @@ +package client + +import ( + "net/http" + + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" +) + +/* +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 +(Local), or when you want to mock out the server for test code (mock). + +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 +} + +// 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 +} + +// baseRPCClient implements the basic RPC method logic without the actual +// underlying RPC call functionality, which is provided by `caller`. +type baseRPCClient struct { + caller rpcclient.RPCCaller +} + +var ( + _ Client = (*HTTP)(nil) + _ Client = (*BatchHTTP)(nil) +) + +// ----------------------------------------------------------------------------- +// HTTP + +// NewHTTP takes a remote endpoint in the form ://: and +// the websocket path (which always seems to be "/websocket") +// The function panics if the provided remote is invalid. +func NewHTTP(remote, wsEndpoint string) *HTTP { + httpClient := rpcclient.DefaultHTTPClient(remote) + return NewHTTPWithClient(remote, wsEndpoint, httpClient) +} + +// NewHTTPWithClient allows for setting a custom http client. See NewHTTP +// The function panics if the provided client is nil or remote is invalid. +func NewHTTPWithClient(remote, wsEndpoint string, client *http.Client) *HTTP { + if client == nil { + panic("nil http.Client provided") + } + rc := rpcclient.NewJSONRPCClientWithHTTPClient(remote, client) + + return &HTTP{ + rpc: rc, + remote: remote, + baseRPCClient: &baseRPCClient{caller: rc}, + } +} + +// 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 diff --git a/tm2/pkg/bft/rpc/client/interface.go b/tm2/pkg/bft/rpc/client/interface.go index a24e95f94bd..0a13853b09b 100644 --- a/tm2/pkg/bft/rpc/client/interface.go +++ b/tm2/pkg/bft/rpc/client/interface.go @@ -32,7 +32,6 @@ import ( // first synchronously consumes the events from the node's synchronous event // switch, or reads logged events from the filesystem. type Client interface { - // service.Service ABCIClient HistoryClient NetworkClient diff --git a/tm2/pkg/bft/rpc/client/localclient.go b/tm2/pkg/bft/rpc/client/local.go similarity index 100% rename from tm2/pkg/bft/rpc/client/localclient.go rename to tm2/pkg/bft/rpc/client/local.go diff --git a/tm2/pkg/bft/rpc/client/ws.go b/tm2/pkg/bft/rpc/client/ws.go new file mode 100644 index 00000000000..d0e88aa8ddc --- /dev/null +++ b/tm2/pkg/bft/rpc/client/ws.go @@ -0,0 +1,19 @@ +package client + +import rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" + +var ( + _ Client = (*WS)(nil) +) + +type WS struct { + rpc *rpcclient.WSClient + + *baseRPCClient +} + +func NewWS(remote, endpoint string) *WS { + return &WS{ + rpc: rpcclient.NewWSClient(remote, endpoint), + } +} diff --git a/tm2/pkg/bft/rpc/lib/client/http_client.go b/tm2/pkg/bft/rpc/lib/client/http_client.go index c02d029f27a..705d0c4c7a7 100644 --- a/tm2/pkg/bft/rpc/lib/client/http_client.go +++ b/tm2/pkg/bft/rpc/lib/client/http_client.go @@ -143,16 +143,16 @@ type JSONRPCClient struct { id types.JSONRPCStringID } -// JSONRPCCaller implementers can facilitate calling the JSON RPC endpoint. -type JSONRPCCaller interface { +// RPCCaller implementers can facilitate calling the JSON RPC endpoint. +type RPCCaller 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) - _ JSONRPCCaller = (*JSONRPCRequestBatch)(nil) + _ RPCCaller = (*JSONRPCClient)(nil) + _ RPCCaller = (*JSONRPCRequestBatch)(nil) ) // NewJSONRPCClient returns a JSONRPCClient pointed at the given address. From 215af3f908e9ed0189c5fc89f52670ce74b64217 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 5 Jan 2024 22:47:20 +0100 Subject: [PATCH 02/26] Add support for batches in the WS client; standardize batch request calling; cleanup --- contribs/gnodev/main.go | 2 +- contribs/gnodev/pkg/dev/node.go | 5 +- contribs/gnodev/pkg/rawterm/rawterm.go | 4 +- gno.land/cmd/gnofaucet/serve.go | 2 +- gno.land/cmd/gnotxsync/export.go | 2 +- gno.land/cmd/gnotxsync/import.go | 2 +- gno.land/pkg/gnoweb/gnoweb.go | 2 +- gnovm/pkg/gnomod/fetch.go | 2 +- tm2/pkg/bft/rpc/client/base.go | 52 ++-- tm2/pkg/bft/rpc/client/examples_test.go | 15 +- tm2/pkg/bft/rpc/client/http.go | 50 ++-- tm2/pkg/bft/rpc/client/mock/client.go | 6 +- tm2/pkg/bft/rpc/client/rpc_test.go | 17 +- tm2/pkg/bft/rpc/client/ws.go | 16 +- tm2/pkg/bft/rpc/lib/client/args_test.go | 4 +- tm2/pkg/bft/rpc/lib/client/batch.go | 118 ++++++++ tm2/pkg/bft/rpc/lib/client/http_client.go | 298 +++++++------------ tm2/pkg/bft/rpc/lib/client/uri_client.go | 64 ++++ tm2/pkg/bft/rpc/lib/client/ws_client.go | 164 ++++++++-- tm2/pkg/bft/rpc/lib/client/ws_client_test.go | 39 ++- tm2/pkg/bft/rpc/lib/rpc_test.go | 74 +++-- tm2/pkg/bft/rpc/lib/server/handlers.go | 4 +- tm2/pkg/bft/rpc/lib/server/handlers_test.go | 4 +- tm2/pkg/bft/rpc/lib/server/http_server.go | 4 +- tm2/pkg/bft/rpc/lib/test/data.json | 9 - tm2/pkg/bft/rpc/lib/test/integration_test.sh | 95 ------ tm2/pkg/bft/rpc/lib/test/main.go | 43 --- tm2/pkg/bft/rpc/lib/types/types.go | 47 +-- tm2/pkg/bft/rpc/lib/types/types_test.go | 2 +- tm2/pkg/bft/rpc/test/helpers.go | 2 +- tm2/pkg/crypto/keys/client/broadcast.go | 2 +- tm2/pkg/crypto/keys/client/query.go | 2 +- 32 files changed, 629 insertions(+), 523 deletions(-) create mode 100644 tm2/pkg/bft/rpc/lib/client/batch.go create mode 100644 tm2/pkg/bft/rpc/lib/client/uri_client.go delete mode 100644 tm2/pkg/bft/rpc/lib/test/data.json delete mode 100755 tm2/pkg/bft/rpc/lib/test/integration_test.sh delete mode 100644 tm2/pkg/bft/rpc/lib/test/main.go diff --git a/contribs/gnodev/main.go b/contribs/gnodev/main.go index 894d8b36738..cdb741719f4 100644 --- a/contribs/gnodev/main.go +++ b/contribs/gnodev/main.go @@ -64,6 +64,7 @@ additional specified paths.`, os.Exit(1) } } + func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { fs.StringVar( &c.webListenerAddr, @@ -92,7 +93,6 @@ func (c *devCfg) RegisterFlags(fs *flag.FlagSet) { defaultDevOptions.noWatch, "do not watch for files change", ) - } func execDev(cfg *devCfg, args []string, io commands.IO) error { diff --git a/contribs/gnodev/pkg/dev/node.go b/contribs/gnodev/pkg/dev/node.go index 6624edc13c1..2e3c5a4b088 100644 --- a/contribs/gnodev/pkg/dev/node.go +++ b/contribs/gnodev/pkg/dev/node.go @@ -17,8 +17,8 @@ import ( "github.com/gnolang/gno/tm2/pkg/crypto" "github.com/gnolang/gno/tm2/pkg/log" "github.com/gnolang/gno/tm2/pkg/std" - //backup "github.com/gnolang/tx-archive/backup/client" - //restore "github.com/gnolang/tx-archive/restore/client" + // backup "github.com/gnolang/tx-archive/backup/client" + // restore "github.com/gnolang/tx-archive/restore/client" ) const gnoDevChainID = "tendermint_test" // XXX: this is hardcoded and cannot be change bellow @@ -160,7 +160,6 @@ func (d *Node) ReloadAll(ctx context.Context) error { } func (d *Node) Reload(ctx context.Context) error { - // save current state state, err := d.saveState(ctx) if err != nil { diff --git a/contribs/gnodev/pkg/rawterm/rawterm.go b/contribs/gnodev/pkg/rawterm/rawterm.go index 8f29e88fc92..f6d6e7534e2 100644 --- a/contribs/gnodev/pkg/rawterm/rawterm.go +++ b/contribs/gnodev/pkg/rawterm/rawterm.go @@ -11,9 +11,7 @@ import ( "golang.org/x/term" ) -var ( - CRLF = []byte{'\r', '\n'} -) +var CRLF = []byte{'\r', '\n'} // rawTerminal wraps an io.Writer, converting \n to \r\n type RawTerm struct { diff --git a/gno.land/cmd/gnofaucet/serve.go b/gno.land/cmd/gnofaucet/serve.go index e00406a6e80..8b248893343 100644 --- a/gno.land/cmd/gnofaucet/serve.go +++ b/gno.land/cmd/gnofaucet/serve.go @@ -173,7 +173,7 @@ func execServe(cfg *config, args []string, io commands.IO) error { if remote == "" || remote == "y" { return errors.New("missing remote url") } - cli := rpcclient.NewHTTP(remote, "/websocket") + cli := rpcclient.NewHTTP(remote) // XXX XXX // Read supply account pubkey. diff --git a/gno.land/cmd/gnotxsync/export.go b/gno.land/cmd/gnotxsync/export.go index f22a4cd22f4..a287eac2b3f 100644 --- a/gno.land/cmd/gnotxsync/export.go +++ b/gno.land/cmd/gnotxsync/export.go @@ -59,7 +59,7 @@ func (c *exportCfg) RegisterFlags(fs *flag.FlagSet) { } func execExport(c *exportCfg) error { - node := client.NewHTTP(c.rootCfg.remote, "/websocket") + node := client.NewHTTP(c.rootCfg.remote) status, err := node.Status() if err != nil { diff --git a/gno.land/cmd/gnotxsync/import.go b/gno.land/cmd/gnotxsync/import.go index 3369b5378b5..258a3f1b9c7 100644 --- a/gno.land/cmd/gnotxsync/import.go +++ b/gno.land/cmd/gnotxsync/import.go @@ -62,7 +62,7 @@ func execImport(ctx context.Context, c *importCfg) error { defer file.Close() // Start the WS connection to the node - node := client.NewHTTP(c.rootCfg.remote, "/websocket") + node := client.NewHTTP(c.rootCfg.remote) index := 0 scanner := bufio.NewScanner(file) diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go index c250842fb50..b5ea22f7c36 100644 --- a/gno.land/pkg/gnoweb/gnoweb.go +++ b/gno.land/pkg/gnoweb/gnoweb.go @@ -421,7 +421,7 @@ func makeRequest(log log.Logger, cfg *Config, qpath string, data []byte) (res *a // Prove: false, XXX } remote := cfg.RemoteAddr - cli := client.NewHTTP(remote, "/websocket") + cli := client.NewHTTP(remote) qres, err := cli.ABCIQueryWithOptions( qpath, data, opts2) if err != nil { diff --git a/gnovm/pkg/gnomod/fetch.go b/gnovm/pkg/gnomod/fetch.go index 6c2b1a63121..1dd11846986 100644 --- a/gnovm/pkg/gnomod/fetch.go +++ b/gnovm/pkg/gnomod/fetch.go @@ -12,7 +12,7 @@ func queryChain(remote string, qpath string, data []byte) (res *abci.ResponseQue // Height: height, XXX // Prove: false, XXX } - cli := client.NewHTTP(remote, "/websocket") + cli := client.NewHTTP(remote) qres, err := cli.ABCIQueryWithOptions(qpath, data, opts2) if err != nil { return nil, err diff --git a/tm2/pkg/bft/rpc/client/base.go b/tm2/pkg/bft/rpc/client/base.go index 939d81362a2..5de961bd301 100644 --- a/tm2/pkg/bft/rpc/client/base.go +++ b/tm2/pkg/bft/rpc/client/base.go @@ -10,7 +10,7 @@ var _ Client = (*baseRPCClient)(nil) func (c *baseRPCClient) Status() (*ctypes.ResultStatus, error) { result := new(ctypes.ResultStatus) - _, err := c.caller.Call("status", map[string]interface{}{}, result) + err := c.caller.Call("status", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "Status") } @@ -19,7 +19,7 @@ func (c *baseRPCClient) Status() (*ctypes.ResultStatus, error) { func (c *baseRPCClient) ABCIInfo() (*ctypes.ResultABCIInfo, error) { result := new(ctypes.ResultABCIInfo) - _, err := c.caller.Call("abci_info", map[string]interface{}{}, result) + err := c.caller.Call("abci_info", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "ABCIInfo") } @@ -32,8 +32,8 @@ func (c *baseRPCClient) ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQ func (c *baseRPCClient) ABCIQueryWithOptions(path string, data []byte, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { result := new(ctypes.ResultABCIQuery) - _, err := c.caller.Call("abci_query", - map[string]interface{}{"path": path, "data": data, "height": opts.Height, "prove": opts.Prove}, + err := c.caller.Call("abci_query", + map[string]any{"path": path, "data": data, "height": opts.Height, "prove": opts.Prove}, result) if err != nil { return nil, errors.Wrap(err, "ABCIQuery") @@ -43,7 +43,7 @@ func (c *baseRPCClient) ABCIQueryWithOptions(path string, data []byte, opts ABCI func (c *baseRPCClient) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { result := new(ctypes.ResultBroadcastTxCommit) - _, err := c.caller.Call("broadcast_tx_commit", map[string]interface{}{"tx": tx}, result) + err := c.caller.Call("broadcast_tx_commit", map[string]any{"tx": tx}, result) if err != nil { return nil, errors.Wrap(err, "broadcast_tx_commit") } @@ -60,7 +60,7 @@ func (c *baseRPCClient) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, func (c *baseRPCClient) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { result := new(ctypes.ResultBroadcastTx) - _, err := c.caller.Call(route, map[string]interface{}{"tx": tx}, result) + err := c.caller.Call(route, map[string]any{"tx": tx}, result) if err != nil { return nil, errors.Wrap(err, route) } @@ -69,7 +69,7 @@ func (c *baseRPCClient) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBr func (c *baseRPCClient) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) { result := new(ctypes.ResultUnconfirmedTxs) - _, err := c.caller.Call("unconfirmed_txs", map[string]interface{}{"limit": limit}, result) + err := c.caller.Call("unconfirmed_txs", map[string]any{"limit": limit}, result) if err != nil { return nil, errors.Wrap(err, "unconfirmed_txs") } @@ -78,7 +78,7 @@ func (c *baseRPCClient) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, func (c *baseRPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { result := new(ctypes.ResultUnconfirmedTxs) - _, err := c.caller.Call("num_unconfirmed_txs", map[string]interface{}{}, result) + err := c.caller.Call("num_unconfirmed_txs", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "num_unconfirmed_txs") } @@ -87,7 +87,7 @@ func (c *baseRPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error func (c *baseRPCClient) NetInfo() (*ctypes.ResultNetInfo, error) { result := new(ctypes.ResultNetInfo) - _, err := c.caller.Call("net_info", map[string]interface{}{}, result) + err := c.caller.Call("net_info", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "NetInfo") } @@ -96,7 +96,7 @@ func (c *baseRPCClient) NetInfo() (*ctypes.ResultNetInfo, error) { func (c *baseRPCClient) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { result := new(ctypes.ResultDumpConsensusState) - _, err := c.caller.Call("dump_consensus_state", map[string]interface{}{}, result) + err := c.caller.Call("dump_consensus_state", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "DumpConsensusState") } @@ -105,7 +105,7 @@ func (c *baseRPCClient) DumpConsensusState() (*ctypes.ResultDumpConsensusState, func (c *baseRPCClient) ConsensusState() (*ctypes.ResultConsensusState, error) { result := new(ctypes.ResultConsensusState) - _, err := c.caller.Call("consensus_state", map[string]interface{}{}, result) + err := c.caller.Call("consensus_state", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "ConsensusState") } @@ -115,9 +115,9 @@ func (c *baseRPCClient) ConsensusState() (*ctypes.ResultConsensusState, error) { func (c *baseRPCClient) ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, error) { result := new(ctypes.ResultConsensusParams) - if _, err := c.caller.Call( + if err := c.caller.Call( "consensus_params", - map[string]interface{}{ + map[string]any{ "height": height, }, result, @@ -130,7 +130,7 @@ func (c *baseRPCClient) ConsensusParams(height *int64) (*ctypes.ResultConsensusP func (c *baseRPCClient) Health() (*ctypes.ResultHealth, error) { result := new(ctypes.ResultHealth) - _, err := c.caller.Call("health", map[string]interface{}{}, result) + err := c.caller.Call("health", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "Health") } @@ -139,8 +139,8 @@ func (c *baseRPCClient) Health() (*ctypes.ResultHealth, error) { func (c *baseRPCClient) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { result := new(ctypes.ResultBlockchainInfo) - _, err := c.caller.Call("blockchain", - map[string]interface{}{"minHeight": minHeight, "maxHeight": maxHeight}, + err := c.caller.Call("blockchain", + map[string]any{"minHeight": minHeight, "maxHeight": maxHeight}, result) if err != nil { return nil, errors.Wrap(err, "BlockchainInfo") @@ -150,7 +150,7 @@ func (c *baseRPCClient) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.Resu func (c *baseRPCClient) Genesis() (*ctypes.ResultGenesis, error) { result := new(ctypes.ResultGenesis) - _, err := c.caller.Call("genesis", map[string]interface{}{}, result) + err := c.caller.Call("genesis", map[string]any{}, result) if err != nil { return nil, errors.Wrap(err, "Genesis") } @@ -159,7 +159,7 @@ func (c *baseRPCClient) Genesis() (*ctypes.ResultGenesis, error) { func (c *baseRPCClient) Block(height *int64) (*ctypes.ResultBlock, error) { result := new(ctypes.ResultBlock) - _, err := c.caller.Call("block", map[string]interface{}{"height": height}, result) + err := c.caller.Call("block", map[string]any{"height": height}, result) if err != nil { return nil, errors.Wrap(err, "Block") } @@ -168,7 +168,7 @@ func (c *baseRPCClient) Block(height *int64) (*ctypes.ResultBlock, error) { func (c *baseRPCClient) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { result := new(ctypes.ResultBlockResults) - _, err := c.caller.Call("block_results", map[string]interface{}{"height": height}, result) + err := c.caller.Call("block_results", map[string]any{"height": height}, result) if err != nil { return nil, errors.Wrap(err, "Block Result") } @@ -177,7 +177,7 @@ func (c *baseRPCClient) BlockResults(height *int64) (*ctypes.ResultBlockResults, func (c *baseRPCClient) Commit(height *int64) (*ctypes.ResultCommit, error) { result := new(ctypes.ResultCommit) - _, err := c.caller.Call("commit", map[string]interface{}{"height": height}, result) + err := c.caller.Call("commit", map[string]any{"height": height}, result) if err != nil { return nil, errors.Wrap(err, "Commit") } @@ -186,11 +186,11 @@ func (c *baseRPCClient) Commit(height *int64) (*ctypes.ResultCommit, error) { func (c *baseRPCClient) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { result := new(ctypes.ResultTx) - params := map[string]interface{}{ + params := map[string]any{ "hash": hash, "prove": prove, } - _, err := c.caller.Call("tx", params, result) + err := c.caller.Call("tx", params, result) if err != nil { return nil, errors.Wrap(err, "Tx") } @@ -199,13 +199,13 @@ func (c *baseRPCClient) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { func (c *baseRPCClient) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { result := new(ctypes.ResultTxSearch) - params := map[string]interface{}{ + params := map[string]any{ "query": query, "prove": prove, "page": page, "per_page": perPage, } - _, err := c.caller.Call("tx_search", params, result) + err := c.caller.Call("tx_search", params, result) if err != nil { return nil, errors.Wrap(err, "TxSearch") } @@ -214,11 +214,11 @@ func (c *baseRPCClient) TxSearch(query string, prove bool, page, perPage int) (* func (c *baseRPCClient) Validators(height *int64) (*ctypes.ResultValidators, error) { result := new(ctypes.ResultValidators) - params := map[string]interface{}{} + params := map[string]any{} if height != nil { params["height"] = height } - _, err := c.caller.Call("validators", params, result) + err := c.caller.Call("validators", params, result) if err != nil { return nil, errors.Wrap(err, "Validators") } diff --git a/tm2/pkg/bft/rpc/client/examples_test.go b/tm2/pkg/bft/rpc/client/examples_test.go index bad20cc9b4f..b494c229d0e 100644 --- a/tm2/pkg/bft/rpc/client/examples_test.go +++ b/tm2/pkg/bft/rpc/client/examples_test.go @@ -2,6 +2,7 @@ package client_test import ( "bytes" + "context" "fmt" "github.com/gnolang/gno/tm2/pkg/bft/abci/example/kvstore" @@ -18,7 +19,7 @@ func ExampleHTTP_simple() { // Create our RPC client rpcAddr := rpctest.GetConfig().RPC.ListenAddress - c := client.NewHTTP(rpcAddr, "/websocket") + c := client.NewHTTP(rpcAddr) // Create a transaction k := []byte("name") @@ -68,7 +69,7 @@ func ExampleHTTP_batching() { // Create our RPC client rpcAddr := rpctest.GetConfig().RPC.ListenAddress - c := client.NewHTTP(rpcAddr, "/websocket") + c := client.NewHTTP(rpcAddr) // Create our two transactions k1 := []byte("firstName") @@ -81,7 +82,7 @@ func ExampleHTTP_batching() { txs := [][]byte{tx1, tx2} - // Create a new batch + // Create a new rpcBatch batch := c.NewBatch() // Queue up our transactions @@ -91,12 +92,12 @@ func ExampleHTTP_batching() { } } - // Send the batch of 2 transactions - if _, err := batch.Send(); err != nil { + // Send the rpcBatch of 2 transactions + if _, err := batch.Send(context.Background()); err != nil { panic(err) } - // Now let's query for the original results as a batch + // Now let's query for the original results as a rpcBatch keys := [][]byte{k1, k2} for _, key := range keys { if _, err := batch.ABCIQuery("/key", key); err != nil { @@ -105,7 +106,7 @@ func ExampleHTTP_batching() { } // Send the 2 queries and keep the results - results, err := batch.Send() + results, err := batch.Send(context.Background()) if err != nil { panic(err) } diff --git a/tm2/pkg/bft/rpc/client/http.go b/tm2/pkg/bft/rpc/client/http.go index 4e73cb8f7c7..714b83cc1b6 100644 --- a/tm2/pkg/bft/rpc/client/http.go +++ b/tm2/pkg/bft/rpc/client/http.go @@ -1,6 +1,7 @@ package client import ( + "context" "net/http" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" @@ -25,7 +26,7 @@ type HTTP struct { *baseRPCClient } -// BatchHTTP provides the same interface as `HTTP`, but allows for batching of +// Batch 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. @@ -35,8 +36,8 @@ type HTTP struct { // 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 +type Batch struct { + rpcBatch *rpcclient.RPCRequestBatch *baseRPCClient } @@ -48,23 +49,22 @@ type baseRPCClient struct { var ( _ Client = (*HTTP)(nil) - _ Client = (*BatchHTTP)(nil) + _ Client = (*Batch)(nil) ) // ----------------------------------------------------------------------------- // HTTP -// NewHTTP takes a remote endpoint in the form ://: and -// the websocket path (which always seems to be "/websocket") +// NewHTTP takes a remote endpoint in the form ://: // The function panics if the provided remote is invalid. -func NewHTTP(remote, wsEndpoint string) *HTTP { +func NewHTTP(remote string) *HTTP { httpClient := rpcclient.DefaultHTTPClient(remote) - return NewHTTPWithClient(remote, wsEndpoint, httpClient) + return NewHTTPWithClient(remote, httpClient) } // NewHTTPWithClient allows for setting a custom http client. See NewHTTP // The function panics if the provided client is nil or remote is invalid. -func NewHTTPWithClient(remote, wsEndpoint string, client *http.Client) *HTTP { +func NewHTTPWithClient(remote string, client *http.Client) *HTTP { if client == nil { panic("nil http.Client provided") } @@ -77,38 +77,32 @@ func NewHTTPWithClient(remote, wsEndpoint string, client *http.Client) *HTTP { } } -// NewBatch creates a new batch client for this HTTP client. -func (c *HTTP) NewBatch() *BatchHTTP { - rpcBatch := c.rpc.NewRequestBatch() - return &BatchHTTP{ - rpcBatch: rpcBatch, +// NewBatch creates a new rpcBatch client for this HTTP client. +func (c *HTTP) NewBatch() *Batch { + batch := rpcclient.NewRPCRequestBatch(c.rpc) + return &Batch{ + rpcBatch: batch, baseRPCClient: &baseRPCClient{ - caller: rpcBatch, + caller: batch, }, } } -// ----------------------------------------------------------------------------- -// BatchHTTP - -// Send is a convenience function for an HTTP batch that will trigger the +// Send is a convenience function for an HTTP rpcBatch 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() +// from each request in the sent rpcBatch. +func (b *Batch) Send(ctx context.Context) ([]any, error) { + return b.rpcBatch.Send(ctx) } -// Clear will empty out this batch of requests and return the number of requests +// Clear will empty out this rpcBatch of requests and return the number of requests // that were cleared out. -func (b *BatchHTTP) Clear() int { +func (b *Batch) Clear() int { return b.rpcBatch.Clear() } // Count returns the number of enqueued requests waiting to be sent. -func (b *BatchHTTP) Count() int { +func (b *Batch) Count() int { return b.rpcBatch.Count() } - -// ----------------------------------------------------------------------------- -// baseRPCClient diff --git a/tm2/pkg/bft/rpc/client/mock/client.go b/tm2/pkg/bft/rpc/client/mock/client.go index 46db69debb3..dfb629daaec 100644 --- a/tm2/pkg/bft/rpc/client/mock/client.go +++ b/tm2/pkg/bft/rpc/client/mock/client.go @@ -45,8 +45,8 @@ var _ client.Client = Client{} // It can also be used to configure mock responses. type Call struct { Name string - Args interface{} - Response interface{} + Args any + Response any Error error } @@ -56,7 +56,7 @@ type Call struct { // When configuring a response, if only one of Response or Error is // set then that will always be returned. If both are set, then // we return Response if the Args match the set args, Error otherwise. -func (c Call) GetResponse(args interface{}) (interface{}, error) { +func (c Call) GetResponse(args any) (any, error) { // handle the case with no response if c.Response == nil { if c.Error == nil { diff --git a/tm2/pkg/bft/rpc/client/rpc_test.go b/tm2/pkg/bft/rpc/client/rpc_test.go index e09ae8d4466..deccb1a00de 100644 --- a/tm2/pkg/bft/rpc/client/rpc_test.go +++ b/tm2/pkg/bft/rpc/client/rpc_test.go @@ -1,6 +1,7 @@ package client_test import ( + "context" "net/http" "strings" "sync" @@ -19,7 +20,7 @@ import ( func getHTTPClient() *client.HTTP { rpcAddr := rpctest.GetConfig().RPC.ListenAddress - return client.NewHTTP(rpcAddr, "/websocket") + return client.NewHTTP(rpcAddr) } func getLocalClient() *client.Local { @@ -38,7 +39,7 @@ func TestNilCustomHTTPClient(t *testing.T) { t.Parallel() require.Panics(t, func() { - client.NewHTTPWithClient("http://example.com", "/websocket", nil) + client.NewHTTPWithClient("http://example.com", nil) }) require.Panics(t, func() { rpcclient.NewJSONRPCClientWithHTTPClient("http://example.com", nil) @@ -49,7 +50,7 @@ func TestCustomHTTPClient(t *testing.T) { t.Parallel() remote := rpctest.GetConfig().RPC.ListenAddress - c := client.NewHTTPWithClient(remote, "/websocket", http.DefaultClient) + c := client.NewHTTPWithClient(remote, http.DefaultClient) status, err := c.Status() require.NoError(t, err) require.NotNil(t, status) @@ -508,7 +509,7 @@ func testBatchedJSONRPCCalls(t *testing.T, c *client.HTTP) { r2, err := batch.BroadcastTxCommit(tx2) require.NoError(t, err) require.Equal(t, 2, batch.Count()) - bresults, err := batch.Send() + bresults, err := batch.Send(context.Background()) require.NoError(t, err) require.Len(t, bresults, 2) require.Equal(t, 0, batch.Count()) @@ -528,7 +529,7 @@ func testBatchedJSONRPCCalls(t *testing.T, c *client.HTTP) { q2, err := batch.ABCIQuery("/key", k2) require.NoError(t, err) require.Equal(t, 2, batch.Count()) - qresults, err := batch.Send() + qresults, err := batch.Send(context.Background()) require.NoError(t, err) require.Len(t, qresults, 2) require.Equal(t, 0, batch.Count()) @@ -571,8 +572,8 @@ 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") + _, err := batch.Send(context.Background()) + require.Error(t, err, "sending an empty rpcBatch of JSON RPC requests should result in an error") } func TestClearingEmptyJSONRPCRequestBatch(t *testing.T) { @@ -580,7 +581,7 @@ 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") + require.Zero(t, batch.Clear(), "clearing an empty rpcBatch of JSON RPC requests should result in a 0 result") } func TestConcurrentJSONRPCBatching(t *testing.T) { diff --git a/tm2/pkg/bft/rpc/client/ws.go b/tm2/pkg/bft/rpc/client/ws.go index d0e88aa8ddc..a3514f851cd 100644 --- a/tm2/pkg/bft/rpc/client/ws.go +++ b/tm2/pkg/bft/rpc/client/ws.go @@ -2,9 +2,7 @@ package client import rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" -var ( - _ Client = (*WS)(nil) -) +var _ Client = (*WS)(nil) type WS struct { rpc *rpcclient.WSClient @@ -17,3 +15,15 @@ func NewWS(remote, endpoint string) *WS { rpc: rpcclient.NewWSClient(remote, endpoint), } } + +// NewBatch creates a new rpcBatch client for this HTTP client. +func (c *WS) NewBatch() *Batch { + batch := rpcclient.NewRPCRequestBatch(c.rpc) + + return &Batch{ + rpcBatch: batch, + baseRPCClient: &baseRPCClient{ + caller: batch, + }, + } +} diff --git a/tm2/pkg/bft/rpc/lib/client/args_test.go b/tm2/pkg/bft/rpc/lib/client/args_test.go index 2a7e749f094..8119cb73b36 100644 --- a/tm2/pkg/bft/rpc/lib/client/args_test.go +++ b/tm2/pkg/bft/rpc/lib/client/args_test.go @@ -21,7 +21,7 @@ func TestArgToJSON(t *testing.T) { require := require.New(t) cases := []struct { - input interface{} + input any expected string }{ {[]byte("1234"), "0x31323334"}, @@ -30,7 +30,7 @@ func TestArgToJSON(t *testing.T) { } for i, tc := range cases { - args := map[string]interface{}{"data": tc.input} + args := map[string]any{"data": tc.input} err := argsToJSON(args) require.Nil(err, "%d: %+v", i, err) require.Equal(1, len(args), "%d", i) diff --git a/tm2/pkg/bft/rpc/lib/client/batch.go b/tm2/pkg/bft/rpc/lib/client/batch.go new file mode 100644 index 00000000000..ea734ffede9 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/batch.go @@ -0,0 +1,118 @@ +package rpcclient + +import ( + "context" + "fmt" + "sync" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/random" +) + +type BatchClient interface { + SendBatch(ctx context.Context, requests wrappedRPCRequests) (types.RPCResponses, error) + GetIDPrefix() types.JSONRPCID +} + +var _ RPCCaller = (*RPCRequestBatch)(nil) + +// RPCRequestBatch 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 RPCRequestBatch struct { + sync.Mutex + + client BatchClient + requests wrappedRPCRequests +} + +// NewRPCRequestBatch creates a new +func NewRPCRequestBatch(client BatchClient) *RPCRequestBatch { + return &RPCRequestBatch{ + client: client, + requests: make(wrappedRPCRequests, 0), + } +} + +// Count returns the number of enqueued requests waiting to be sent +func (b *RPCRequestBatch) Count() int { + b.Lock() + defer b.Unlock() + + return len(b.requests) +} + +// Clear empties out the request batch +func (b *RPCRequestBatch) Clear() int { + b.Lock() + defer b.Unlock() + + return b.clear() +} + +func (b *RPCRequestBatch) clear() int { + count := len(b.requests) + b.requests = make(wrappedRPCRequests, 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 *RPCRequestBatch) Send(ctx context.Context) ([]any, error) { + b.Lock() + defer func() { + b.clear() + b.Unlock() + }() + + requests := make(types.RPCRequests, 0, len(b.requests)) + results := make([]any, 0, len(b.requests)) + + for _, req := range b.requests { + requests = append(requests, req.request) + results = append(results, req.result) + } + + responses, err := b.client.SendBatch(ctx, b.requests) + if err != nil { + return nil, err + } + + if err := unmarshalResponsesIntoResults(requests, responses, results); err != nil { + return nil, err + } + + return results, nil +} + +// Call enqueues a request to call the given RPC method with the specified parameters +func (b *RPCRequestBatch) Call(method string, params map[string]any, result any) error { + // Assuming this is sufficiently random, there shouldn't be any problems. + // However, using uuid for any kind of ID generation is always preferred + id := types.JSONRPCStringID( + fmt.Sprintf("%s-%s", b.client.GetIDPrefix(), random.RandStr(8)), + ) + + request, err := types.MapToRequest(id, method, params) + if err != nil { + return err + } + + b.enqueue( + &wrappedRPCRequest{ + request: request, + result: result, + }, + ) + + return nil +} + +func (b *RPCRequestBatch) enqueue(req *wrappedRPCRequest) { + b.Lock() + defer b.Unlock() + + b.requests = append(b.requests, req) +} diff --git a/tm2/pkg/bft/rpc/lib/client/http_client.go b/tm2/pkg/bft/rpc/lib/client/http_client.go index 705d0c4c7a7..6bdf6598977 100644 --- a/tm2/pkg/bft/rpc/lib/client/http_client.go +++ b/tm2/pkg/bft/rpc/lib/client/http_client.go @@ -2,6 +2,7 @@ package rpcclient import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -10,7 +11,6 @@ import ( "net/url" "reflect" "strings" - "sync" "github.com/gnolang/gno/tm2/pkg/amino" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" @@ -28,7 +28,7 @@ const ( // HTTPClient is a common interface for JSONRPCClient and URIClient. type HTTPClient interface { - Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) + Call(method string, params map[string]any, result any) error } // protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp") @@ -119,40 +119,44 @@ func DefaultHTTPClient(remoteAddr 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. +// JSONRPCClient takes params as a slice +type JSONRPCClient struct { + address string + client *http.Client + idPrefix types.JSONRPCStringID } -// 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 +// RPCCaller implementers can facilitate calling the JSON RPC endpoint. +type RPCCaller interface { + Call(method string, params map[string]any, result any) error +} - mtx sync.Mutex - requests []*jsonRPCBufferedRequest +// wrappedRPCRequest encapsulates a single buffered request, as well as its +// anticipated response structure +type wrappedRPCRequest struct { + request types.RPCRequest + result any // The result will be deserialized into this object (Amino) } -// JSONRPCClient takes params as a slice -type JSONRPCClient struct { - address string - client *http.Client - id types.JSONRPCStringID +type wrappedRPCRequests []*wrappedRPCRequest + +func (w *wrappedRPCRequest) extractRPCRequest() types.RPCRequest { + return w.request } -// RPCCaller implementers can facilitate calling the JSON RPC endpoint. -type RPCCaller interface { - Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) +func (w *wrappedRPCRequests) extractRPCRequests() types.RPCRequests { + requests := make([]types.RPCRequest, 0, len(*w)) + + for _, wrappedRequest := range *w { + requests = append(requests, wrappedRequest.request) + } + + return requests } -// Both JSONRPCClient and JSONRPCRequestBatch can facilitate calls to the JSON -// RPC endpoint. var ( - _ RPCCaller = (*JSONRPCClient)(nil) - _ RPCCaller = (*JSONRPCRequestBatch)(nil) + _ RPCCaller = (*JSONRPCClient)(nil) + _ BatchClient = (*JSONRPCClient)(nil) ) // NewJSONRPCClient returns a JSONRPCClient pointed at the given address. @@ -173,61 +177,67 @@ func NewJSONRPCClientWithHTTPClient(remote string, client *http.Client) *JSONRPC } return &JSONRPCClient{ - address: clientAddress, - client: client, - id: types.JSONRPCStringID("jsonrpc-client-" + random.RandStr(8)), + address: clientAddress, + client: client, + idPrefix: types.JSONRPCStringID("jsonrpc-client-" + random.RandStr(8)), } } // 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.id, method, params) +func (c *JSONRPCClient) Call(method string, params map[string]any, result any) error { + id := generateRequestID(c.idPrefix) + + request, err := types.MapToRequest(id, method, params) if err != nil { - return nil, err + return err } requestBytes, err := json.Marshal(request) if err != nil { - return nil, err + return err } requestBuf := bytes.NewBuffer(requestBytes) httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) if err != nil { - return nil, err + return err } defer httpResponse.Body.Close() //nolint: errcheck if !statusOK(httpResponse.StatusCode) { - return nil, errors.New("server at '%s' returned %s", c.address, httpResponse.Status) + return errors.New("server at '%s' returned %s", c.address, httpResponse.Status) } responseBytes, err := io.ReadAll(httpResponse.Body) if err != nil { - return nil, err + return err } - return unmarshalResponseBytes(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, + var response types.RPCResponse + + err = json.Unmarshal(responseBytes, &response) + if err != nil { + return errors.Wrap(err, "error unmarshalling rpc response") + } + + if response.Error != nil { + return errors.Wrap(response.Error, "response error") } + + return unmarshalResponseIntoResult(&response, id, result) } -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) +func (c *JSONRPCClient) SendBatch(_ context.Context, wrappedRequests wrappedRPCRequests) (types.RPCResponses, error) { + requests := make(types.RPCRequests, 0, len(wrappedRequests)) + for _, request := range wrappedRequests { + requests = append(requests, request.request) } + // serialize the array of requests into a single JSON object - requestBytes, err := json.Marshal(reqs) + requestBytes, err := json.Marshal(requests) if err != nil { return nil, err } + httpResponse, err := c.client.Post(c.address, "text/json", bytes.NewBuffer(requestBytes)) if err != nil { return nil, err @@ -239,183 +249,88 @@ func (c *JSONRPCClient) sendBatch(requests []*jsonRPCBufferedRequest) ([]interfa } responseBytes, err := io.ReadAll(httpResponse.Body) - if err != nil { - return nil, err - } - return unmarshalResponseBytesArray(responseBytes, c.id, results) -} -// ------------------------------------------------------------- + var responses types.RPCResponses -// 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.id, method, params) - if err != nil { - return nil, err + if err = json.Unmarshal(responseBytes, &responses); err != nil { + return nil, errors.Wrap(err, "error unmarshalling rpc responses") } - b.enqueue(&jsonRPCBufferedRequest{request: request, result: result}) - return result, nil -} - -// ------------------------------------------------------------- -// URI takes params as a map -type URIClient struct { - address string - client *http.Client + return responses, nil } -// The function panics if the provided remote is invalid. -func NewURIClient(remote string) *URIClient { - clientAddress, err := toClientAddress(remote) - if err != nil { - panic(fmt.Sprintf("invalid remote %s: %s", remote, err)) - } - return &URIClient{ - address: clientAddress, - client: DefaultHTTPClient(remote), - } +func (c *JSONRPCClient) GetIDPrefix() types.JSONRPCID { + return c.idPrefix } -func (c *URIClient) Call(method string, params map[string]interface{}, result interface{}) (interface{}, error) { - values, err := argsToURLValues(params) - if err != nil { - return nil, err - } - // log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) - resp, err := c.client.PostForm(c.address+"/"+method, values) - if err != nil { - return nil, err - } - defer resp.Body.Close() //nolint: errcheck - - if !statusOK(resp.StatusCode) { - return nil, errors.New("server at '%s' returned %s", c.address, resp.Status) - } - - responseBytes, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - return unmarshalResponseBytes(responseBytes, "", result) -} - -// ------------------------------------------------ - -func unmarshalResponseBytes(responseBytes []byte, expectedID types.JSONRPCStringID, result interface{}) (interface{}, error) { +func unmarshalResponseIntoResult(response *types.RPCResponse, expectedID types.JSONRPCID, result any) error { // Read response. If rpc/core/types is imported, the result will unmarshal // into the correct type. - // log.Notice("response", "response", string(responseBytes)) - var err error - response := &types.RPCResponse{} - err = json.Unmarshal(responseBytes, response) - if err != nil { - return nil, errors.Wrap(err, "error unmarshalling rpc response") - } - if response.Error != nil { - 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. + // idPrefix: It MUST be the same as the value of the idPrefix member in the Request Object. if err := validateResponseID(response, expectedID); err != nil { - return nil, err + return err } + // Unmarshal the RawMessage into the result. - err = amino.UnmarshalJSON(response.Result, result) - if err != nil { - return nil, errors.Wrap(err, "error unmarshalling rpc response result") + if err := amino.UnmarshalJSON(response.Result, result); err != nil { + return errors.Wrap(err, "error unmarshalling rpc response result") } - return result, nil + + return nil } -func unmarshalResponseBytesArray(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.Wrap(err, "error unmarshalling rpc response") - } +func unmarshalResponsesIntoResults(requests types.RPCRequests, responses types.RPCResponses, results []any) error { // No response error checking here as there may be a mixture of successful - // and unsuccessful responses. - + // and unsuccessful responses if len(results) != len(responses) { - return nil, errors.New("expected %d result objects into which to inject responses, but got %d", len(responses), len(results)) + return fmt.Errorf("expected %d result objects into which to inject responses, but got %d", len(responses), len(results)) } for i, response := range responses { response := response // 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.Wrap(err, "failed to validate response ID in response %d", i) + // idPrefix: It MUST be the same as the value of the idPrefix member in the Request Object. + + // This validation is super sketchy. Why do this here? + // This validation passes iff the server returns batch responses + // in the same order as the batch request + if err := validateResponseID(&response, requests[i].ID); err != nil { + return errors.Wrap(err, "failed to validate response ID in response %d", i) } if err := amino.UnmarshalJSON(responses[i].Result, results[i]); err != nil { - return nil, errors.Wrap(err, "error unmarshalling rpc response result") + return errors.Wrap(err, "error unmarshalling rpc response result") } } - return results, nil + + return nil } -func validateResponseID(res *types.RPCResponse, expectedID types.JSONRPCStringID) error { +func validateResponseID(res *types.RPCResponse, expectedID types.JSONRPCID) error { + _, isNumValue := expectedID.(types.JSONRPCIntID) + stringValue, isStringValue := expectedID.(types.JSONRPCStringID) + + if !isNumValue && !isStringValue { + return errors.New("invalid expected ID") + } + // we only validate a response ID if the expected ID is non-empty - if len(expectedID) == 0 { + if isStringValue && len(stringValue) == 0 { return nil } + if res.ID == nil { return errors.New("missing ID in response") } - id, ok := res.ID.(types.JSONRPCStringID) - if !ok { - return errors.New("expected ID string in response but got: %v", id) - } - if expectedID != id { - return errors.New("response ID (%s) does not match request ID (%s)", id, expectedID) + + if expectedID != res.ID { + return fmt.Errorf("response ID (%s) does not match request ID (%s)", res.ID, expectedID) } + return nil } -func argsToURLValues(args map[string]interface{}) (url.Values, error) { +func argsToURLValues(args map[string]any) (url.Values, error) { values := make(url.Values) if len(args) == 0 { return values, nil @@ -430,7 +345,7 @@ func argsToURLValues(args map[string]interface{}) (url.Values, error) { return values, nil } -func argsToJSON(args map[string]interface{}) error { +func argsToJSON(args map[string]any) error { for k, v := range args { rt := reflect.TypeOf(v) isByteSlice := rt.Kind() == reflect.Slice && rt.Elem().Kind() == reflect.Uint8 @@ -450,3 +365,10 @@ func argsToJSON(args map[string]interface{}) error { } func statusOK(code int) bool { return code >= 200 && code <= 299 } + +// generateRequestID generates a unique request ID, using the prefix +// Assuming this is sufficiently random, there shouldn't be any problems. +// However, using uuid for any kind of ID generation is always preferred +func generateRequestID(prefix types.JSONRPCID) types.JSONRPCID { + return types.JSONRPCStringID(fmt.Sprintf("%s-%s", prefix, random.RandStr(8))) +} diff --git a/tm2/pkg/bft/rpc/lib/client/uri_client.go b/tm2/pkg/bft/rpc/lib/client/uri_client.go new file mode 100644 index 00000000000..bfce993a84e --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/uri_client.go @@ -0,0 +1,64 @@ +package rpcclient + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/errors" +) + +// URI takes params as a map +type URIClient struct { + address string + client *http.Client +} + +// The function panics if the provided remote is invalid. +func NewURIClient(remote string) *URIClient { + clientAddress, err := toClientAddress(remote) + if err != nil { + panic(fmt.Sprintf("invalid remote %s: %s", remote, err)) + } + return &URIClient{ + address: clientAddress, + client: DefaultHTTPClient(remote), + } +} + +func (c *URIClient) Call(method string, params map[string]any, result any) error { + values, err := argsToURLValues(params) + if err != nil { + return err + } + // log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) + resp, err := c.client.PostForm(c.address+"/"+method, values) + if err != nil { + return err + } + defer resp.Body.Close() //nolint: errcheck + + if !statusOK(resp.StatusCode) { + return errors.New("server at '%s' returned %s", c.address, resp.Status) + } + + responseBytes, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + var response types.RPCResponse + + err = json.Unmarshal(responseBytes, &response) + if err != nil { + return errors.Wrap(err, "error unmarshalling rpc response") + } + + if response.Error != nil { + return errors.Wrap(response.Error, "response error") + } + + return unmarshalResponseIntoResult(&response, types.JSONRPCStringID(""), result) +} diff --git a/tm2/pkg/bft/rpc/lib/client/ws_client.go b/tm2/pkg/bft/rpc/lib/client/ws_client.go index 4e159a3e3dc..0a9b43cc9b0 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws_client.go +++ b/tm2/pkg/bft/rpc/lib/client/ws_client.go @@ -23,6 +23,8 @@ const ( defaultPingPeriod = 0 ) +var _ BatchClient = (*WSClient)(nil) + // WSClient is a WebSocket client. The methods of WSClient are safe for use by // multiple goroutines. type WSClient struct { @@ -35,16 +37,16 @@ type WSClient struct { Dialer func(string, string) (net.Conn, error) // Single user facing channel to read RPCResponses from, closed only when the client is being stopped. - ResponsesCh chan types.RPCResponse + ResponsesCh chan types.RPCResponses // Callback, which will be called each time after successful reconnect. onReconnect func() // internal channels - send chan types.RPCRequest // user requests - backlog chan types.RPCRequest // stores a single user request received during a conn failure - reconnectAfter chan error // reconnect requests - readRoutineQuit chan struct{} // a way for readRoutine to close writeRoutine + send chan wrappedRPCRequests // user requests + backlog chan wrappedRPCRequests // stores user requests received during a conn failure + reconnectAfter chan error // reconnect requests + readRoutineQuit chan struct{} // a way for readRoutine to close writeRoutine wg sync.WaitGroup @@ -66,6 +68,31 @@ type WSClient struct { // Support both ws and wss protocols protocol string + + idPrefix types.JSONRPCID + + // Since requests are sent in, and responses + // are parsed asynchronously in this implementation, + // the thread that is parsing the response result + // needs to perform Amino decoding. + // The thread that spawn the request, and the thread that parses + // the request do not live in the same context. + // This information (the object type) needs to be available at this moment, + // so a map is used to temporarily store those types + requestResultsMap map[types.JSONRPCID]any +} + +func (c *WSClient) SendBatch(ctx context.Context, requests wrappedRPCRequests) (types.RPCResponses, error) { + if err := c.Send(ctx, requests...); err != nil { + return nil, err + } + + select { + case <-ctx.Done(): + return nil, errors.New("timed out") + case responses := <-c.ResponsesCh: + return responses, nil + } } // NewWSClient returns a new client. See the commentary on the func(*WSClient) @@ -92,6 +119,8 @@ func NewWSClient(remoteAddr, endpoint string, options ...func(*WSClient)) *WSCli writeWait: defaultWriteWait, pingPeriod: defaultPingPeriod, protocol: protocol, + idPrefix: types.JSONRPCStringID("ws-client-" + random.RandStr(8)), + requestResultsMap: make(map[types.JSONRPCID]any), } c.BaseService = *service.NewBaseService(nil, "WSClient", c) for _, option := range options { @@ -145,6 +174,10 @@ func (c *WSClient) String() string { return fmt.Sprintf("%s (%s)", c.Address, c.Endpoint) } +func (c *WSClient) GetIDPrefix() types.JSONRPCID { + return c.idPrefix +} + // OnStart implements service.Service by dialing a server and creating read and // write routines. func (c *WSClient) OnStart() error { @@ -153,15 +186,15 @@ func (c *WSClient) OnStart() error { return err } - c.ResponsesCh = make(chan types.RPCResponse) + c.ResponsesCh = make(chan types.RPCResponses) - c.send = make(chan types.RPCRequest) + c.send = make(chan wrappedRPCRequests) // 1 additional error may come from the read/write // goroutine depending on which failed first. c.reconnectAfter = make(chan error, 1) // capacity for 1 request. a user won't be able to send more because the send // channel is unbuffered. - c.backlog = make(chan types.RPCRequest, 1) + c.backlog = make(chan wrappedRPCRequests, 1) c.startReadWriteRoutines() go c.reconnectRoutine() @@ -197,10 +230,10 @@ func (c *WSClient) IsActive() bool { // Send the given RPC request to the server. Results will be available on // ResponsesCh, errors, if any, on ErrorsCh. Will block until send succeeds or // ctx.Done is closed. -func (c *WSClient) Send(ctx context.Context, request types.RPCRequest) error { +func (c *WSClient) Send(ctx context.Context, requests ...*wrappedRPCRequest) error { select { - case c.send <- request: - c.Logger.Info("sent a request", "req", request) + case c.send <- requests: + c.Logger.Info("sent requests", "num", len(requests)) return nil case <-ctx.Done(): return ctx.Err() @@ -208,22 +241,40 @@ func (c *WSClient) Send(ctx context.Context, request types.RPCRequest) error { } // Call the given method. See Send description. -func (c *WSClient) Call(ctx context.Context, method string, params map[string]interface{}) error { - request, err := types.MapToRequest(types.JSONRPCStringID("ws-client"), method, params) +func (c *WSClient) Call(ctx context.Context, method string, params map[string]any, result any) error { + id := generateRequestID(c.idPrefix) + + request, err := types.MapToRequest(id, method, params) if err != nil { return err } - return c.Send(ctx, request) + + return c.Send( + ctx, + &wrappedRPCRequest{ + request: request, + result: result, + }, + ) } // CallWithArrayParams the given method with params in a form of array. See // Send description. -func (c *WSClient) CallWithArrayParams(ctx context.Context, method string, params []interface{}) error { - request, err := types.ArrayToRequest(types.JSONRPCStringID("ws-client"), method, params) +func (c *WSClient) CallWithArrayParams(ctx context.Context, method string, params []any, result any) error { + id := generateRequestID(c.idPrefix) + + request, err := types.ArrayToRequest(id, method, params) if err != nil { return err } - return c.Send(ctx, request) + + return c.Send( + ctx, + &wrappedRPCRequest{ + request: request, + result: result, + }, + ) } // ----------- @@ -292,20 +343,24 @@ func (c *WSClient) startReadWriteRoutines() { func (c *WSClient) processBacklog() error { select { - case request := <-c.backlog: + case wrappedRequests := <-c.backlog: if c.writeWait > 0 { if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { c.Logger.Error("failed to set write deadline", "err", err) } } - if err := c.conn.WriteJSON(request); err != nil { + + writeData := extractWriteData(wrappedRequests) + + if err := c.conn.WriteJSON(writeData); err != nil { c.Logger.Error("failed to resend request", "err", err) c.reconnectAfter <- err // requeue request - c.backlog <- request + c.backlog <- wrappedRequests return err } - c.Logger.Info("resend a request", "req", request) + + c.Logger.Info("resend a request", "req", writeData) default: } return nil @@ -366,17 +421,25 @@ func (c *WSClient) writeRoutine() { for { select { - case request := <-c.send: + case wrappedRequests := <-c.send: if c.writeWait > 0 { if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { c.Logger.Error("failed to set write deadline", "err", err) } } - if err := c.conn.WriteJSON(request); err != nil { + + // Save the results for later lookups + for _, req := range wrappedRequests { + c.requestResultsMap[req.request.ID] = req.result + } + + writeData := extractWriteData(wrappedRequests) + + if err := c.conn.WriteJSON(writeData); err != nil { c.Logger.Error("failed to send request", "err", err) c.reconnectAfter <- err // add request to the backlog, so we don't lose it - c.backlog <- request + c.backlog <- wrappedRequests return } case <-ticker.C: @@ -449,19 +512,58 @@ func (c *WSClient) readRoutine() { return } - var response types.RPCResponse - err = json.Unmarshal(data, &response) - if err != nil { - c.Logger.Error("failed to parse response", "err", err, "data", string(data)) - continue + var responses types.RPCResponses + + // Try to unmarshal as a batch of responses first + if err := json.Unmarshal(data, &responses); err != nil { + // Try to unmarshal as a single response + var response types.RPCResponse + + if err := json.Unmarshal(data, &response); err != nil { + c.Logger.Error("failed to parse response", "err", err, "data", string(data)) + + continue + } + + result, ok := c.requestResultsMap[response.ID] + if !ok { + c.Logger.Error("response result not set", "id", response.ID, "response", response) + + continue + } + + // Clear the expected result for this ID + delete(c.requestResultsMap, response.ID) + + if err := unmarshalResponseIntoResult(&response, response.ID, result); err != nil { + c.Logger.Error("failed to parse response", "err", err, "data", string(data)) + + continue + } + + responses = types.RPCResponses{response} + } + + for _, userResponse := range responses { + c.Logger.Info("got response", "resp", userResponse.Result) } - c.Logger.Info("got response", "resp", response.Result) + // Combine a non-blocking read on BaseService.Quit with a non-blocking write on ResponsesCh to avoid blocking // c.wg.Wait() in c.Stop(). Note we rely on Quit being closed so that it sends unlimited Quit signals to stop // both readRoutine and writeRoutine select { case <-c.Quit(): - case c.ResponsesCh <- response: + case c.ResponsesCh <- responses: } } } + +// extractWriteData extracts write data (single request or multiple requests) +// from the given wrapped requests set +func extractWriteData(wrappedRequests wrappedRPCRequests) any { + if len(wrappedRequests) == 1 { + return wrappedRequests[0].extractRPCRequest() + } + + return wrappedRequests.extractRPCRequests() +} diff --git a/tm2/pkg/bft/rpc/lib/client/ws_client_test.go b/tm2/pkg/bft/rpc/lib/client/ws_client_test.go index 47c3a50ee63..9f0cccbde6b 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws_client_test.go +++ b/tm2/pkg/bft/rpc/lib/client/ws_client_test.go @@ -36,11 +36,18 @@ func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer conn.Close() //nolint: errcheck for { - messageType, _, err := conn.ReadMessage() + messageType, messageRaw, err := conn.ReadMessage() if err != nil { return } + // Read the message + var request types.RPCRequest + + if err := json.Unmarshal(messageRaw, &request); err != nil { + return + } + h.mtx.RLock() if h.closeConnAfterRead { if err := conn.Close(); err != nil { @@ -49,8 +56,11 @@ func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { } h.mtx.RUnlock() - res := json.RawMessage(`{}`) - emptyRespBytes, _ := json.Marshal(types.RPCResponse{Result: res}) + emptyRespBytes, err := json.Marshal(types.RPCResponse{ + JSONRPC: "2.0", + ID: request.ID, + Result: json.RawMessage(`""`), + }) if err := conn.WriteMessage(messageType, emptyRespBytes); err != nil { return } @@ -154,7 +164,10 @@ func TestWSClientReconnectFailure(t *testing.T) { // provide timeout to avoid blocking ctx, cancel := context.WithTimeout(context.Background(), wsCallTimeout) defer cancel() - if err := c.Call(ctx, "a", make(map[string]interface{})); err != nil { + + var result string + + if err := c.Call(ctx, "a", make(map[string]any), &result); err != nil { t.Error(err) } @@ -183,7 +196,10 @@ func TestNotBlockingOnStop(t *testing.T) { timeout := 2 * time.Second s := httptest.NewServer(&myHandler{}) c := startClient(t, s.Listener.Addr()) - c.Call(context.Background(), "a", make(map[string]interface{})) + + var result string + + c.Call(context.Background(), "a", make(map[string]any), &result) // Let the readRoutine get around to blocking time.Sleep(time.Second) passCh := make(chan struct{}) @@ -215,7 +231,9 @@ func startClient(t *testing.T, addr net.Addr) *WSClient { func call(t *testing.T, method string, c *WSClient) { t.Helper() - err := c.Call(context.Background(), method, make(map[string]interface{})) + var result string + + err := c.Call(context.Background(), method, make(map[string]any), &result) require.NoError(t, err) } @@ -224,7 +242,14 @@ func callWgDoneOnResult(t *testing.T, c *WSClient, wg *sync.WaitGroup) { for { select { - case resp := <-c.ResponsesCh: + case resps := <-c.ResponsesCh: + if len(resps) != 1 { + t.Errorf("invalid number of responses, %d", len(resps)) + + return + } + + resp := resps[0] if resp.Error != nil { t.Errorf("unexpected error: %v", resp.Error) return diff --git a/tm2/pkg/bft/rpc/lib/rpc_test.go b/tm2/pkg/bft/rpc/lib/rpc_test.go index a299417187a..0203d10a24a 100644 --- a/tm2/pkg/bft/rpc/lib/rpc_test.go +++ b/tm2/pkg/bft/rpc/lib/rpc_test.go @@ -158,7 +158,7 @@ func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { "arg": val, } result := new(ResultEcho) - if _, err := cl.Call("echo", params, result); err != nil { + if err := cl.Call("echo", params, result); err != nil { return "", err } return result.Value, nil @@ -169,7 +169,7 @@ func echoIntViaHTTP(cl client.HTTPClient, val int) (int, error) { "arg": val, } result := new(ResultEchoInt) - if _, err := cl.Call("echo_int", params, result); err != nil { + if err := cl.Call("echo_int", params, result); err != nil { return 0, err } return result.Value, nil @@ -180,7 +180,7 @@ func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { "arg": bytes, } result := new(ResultEchoBytes) - if _, err := cl.Call("echo_bytes", params, result); err != nil { + if err := cl.Call("echo_bytes", params, result); err != nil { return []byte{}, err } return result.Value, nil @@ -191,7 +191,7 @@ func echoDataBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { "arg": bytes, } result := new(ResultEchoDataBytes) - if _, err := cl.Call("echo_data_bytes", params, result); err != nil { + if err := cl.Call("echo_data_bytes", params, result); err != nil { return []byte{}, err } return result.Value, nil @@ -225,15 +225,18 @@ func echoViaWS(cl *client.WSClient, val string) (string, error) { params := map[string]interface{}{ "arg": val, } - err := cl.Call(context.Background(), "echo", params) + err := cl.Call(context.Background(), "echo", params, nil) if err != nil { return "", err } - msg := <-cl.ResponsesCh - if msg.Error != nil { - return "", err + msgs := <-cl.ResponsesCh + if len(msgs) != 1 { + return "", fmt.Errorf("invalid number of responses, %d", len(msgs)) } + + msg := msgs[0] + result := new(ResultEcho) err = json.Unmarshal(msg.Result, result) if err != nil { @@ -246,12 +249,17 @@ func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { params := map[string]interface{}{ "arg": bytes, } - err := cl.Call(context.Background(), "echo_bytes", params) + err := cl.Call(context.Background(), "echo_bytes", params, nil) if err != nil { return []byte{}, err } - msg := <-cl.ResponsesCh + msgs := <-cl.ResponsesCh + if len(msgs) != 1 { + return nil, fmt.Errorf("invalid number of responses, %d", len(msgs)) + } + + msg := msgs[0] if msg.Error != nil { return []byte{}, msg.Error } @@ -302,11 +310,11 @@ func TestServersAndClientsBasic(t *testing.T) { } cl1 := client.NewURIClient(tcpServerUnavailableAddr) - _, err := cl1.Call("error", nil, nil) + err := cl1.Call("error", nil, nil) require.EqualError(t, err, "server at 'http://0.0.0.0:47769' returned 418 I'm a teapot") cl2 := client.NewJSONRPCClient(tcpServerUnavailableAddr) - _, err = cl2.Call("error", nil, nil) + err = cl2.Call("error", nil, nil) require.EqualError(t, err, "server at 'http://0.0.0.0:47769' returned 418 I'm a teapot") } @@ -345,18 +353,21 @@ func TestWSNewWSRPCFunc(t *testing.T) { params := map[string]interface{}{ "arg": val, } - err = cl.Call(context.Background(), "echo_ws", params) + err = cl.Call(context.Background(), "echo_ws", params, nil) require.Nil(t, err) - msg := <-cl.ResponsesCh - if msg.Error != nil { - t.Fatal(err) + msgs := <-cl.ResponsesCh + + for _, msg := range msgs { + if msg.Error != nil { + t.Fatal(err) + } + result := new(ResultEcho) + err = json.Unmarshal(msg.Result, result) + require.Nil(t, err) + got := result.Value + assert.Equal(t, got, val) } - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - require.Nil(t, err) - got := result.Value - assert.Equal(t, got, val) } func TestWSHandlesArrayParams(t *testing.T) { @@ -370,18 +381,21 @@ func TestWSHandlesArrayParams(t *testing.T) { val := testVal params := []interface{}{val} - err = cl.CallWithArrayParams(context.Background(), "echo_ws", params) + err = cl.CallWithArrayParams(context.Background(), "echo_ws", params, nil) require.Nil(t, err) - msg := <-cl.ResponsesCh - if msg.Error != nil { - t.Fatalf("%+v", err) + msgs := <-cl.ResponsesCh + + for _, msg := range msgs { + if msg.Error != nil { + t.Fatalf("%+v", err) + } + result := new(ResultEcho) + err = json.Unmarshal(msg.Result, result) + require.Nil(t, err) + got := result.Value + assert.Equal(t, got, val) } - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - require.Nil(t, err) - got := result.Value - assert.Equal(t, got, val) } // TestWSClientPingPong checks that a client & server exchange pings diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index 40543e5f465..7243d2ee4de 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -115,8 +115,8 @@ func makeJSONRPCHandler(funcMap map[string]*RPCFunc, logger log.Logger) http.Han // first try to unmarshal the incoming request as an array of RPC requests var ( - requests []types.RPCRequest - responses []types.RPCResponse + requests types.RPCRequests + responses types.RPCResponses ) if err := json.Unmarshal(b, &requests); err != nil { // next, try to unmarshal as a single request diff --git a/tm2/pkg/bft/rpc/lib/server/handlers_test.go b/tm2/pkg/bft/rpc/lib/server/handlers_test.go index e09a8a7cf96..21d74a6193d 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers_test.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers_test.go @@ -196,7 +196,7 @@ func TestRPCNotificationInBatch(t *testing.T) { continue } - var responses []types.RPCResponse + var responses types.RPCResponses // try to unmarshal an array first err = json.Unmarshal(blob, &responses) if err != nil { @@ -213,7 +213,7 @@ func TestRPCNotificationInBatch(t *testing.T) { continue } // have a single-element result - responses = []types.RPCResponse{response} + responses = types.RPCResponses{response} } } if tt.expectCount != len(responses) { diff --git a/tm2/pkg/bft/rpc/lib/server/http_server.go b/tm2/pkg/bft/rpc/lib/server/http_server.go index feaba1ba938..56ba99650e2 100644 --- a/tm2/pkg/bft/rpc/lib/server/http_server.go +++ b/tm2/pkg/bft/rpc/lib/server/http_server.go @@ -118,7 +118,7 @@ func WriteRPCResponseHTTP(w http.ResponseWriter, res types.RPCResponse) { // 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) { +func WriteRPCResponseArrayHTTP(w http.ResponseWriter, res types.RPCResponses) { if len(res) == 1 { WriteRPCResponseHTTP(w, res[0]) } else { @@ -134,7 +134,7 @@ func WriteRPCResponseArrayHTTP(w http.ResponseWriter, res []types.RPCResponse) { } } -//----------------------------------------------------------------------------- +// ----------------------------------------------------------------------------- // RecoverAndLogHandler wraps an HTTP handler, adding error logging. // If the inner function panics, the outer function recovers, logs, sends an diff --git a/tm2/pkg/bft/rpc/lib/test/data.json b/tm2/pkg/bft/rpc/lib/test/data.json deleted file mode 100644 index 83283ec33fb..00000000000 --- a/tm2/pkg/bft/rpc/lib/test/data.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "jsonrpc": "2.0", - "id": "", - "method": "hello_world", - "params": { - "name": "my_world", - "num": 5 - } -} diff --git a/tm2/pkg/bft/rpc/lib/test/integration_test.sh b/tm2/pkg/bft/rpc/lib/test/integration_test.sh deleted file mode 100755 index 7c23be7d3b9..00000000000 --- a/tm2/pkg/bft/rpc/lib/test/integration_test.sh +++ /dev/null @@ -1,95 +0,0 @@ -#!/usr/bin/env bash -set -e - -# Get the directory of where this script is. -SOURCE="${BASH_SOURCE[0]}" -while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done -DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" - -# Change into that dir because we expect that. -pushd "$DIR" - -echo "==> Building the server" -go build -o rpcserver main.go - -echo "==> (Re)starting the server" -PID=$(pgrep rpcserver || echo "") -if [[ $PID != "" ]]; then - kill -9 "$PID" -fi -./rpcserver & -PID=$! -sleep 2 - -echo "==> simple request" -R1=$(curl -s 'http://localhost:8008/hello_world?name="my_world"&num=5') -R2=$(curl -s --data @data.json http://localhost:8008) -if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - echo "FAIL" - exit 1 -else - echo "OK" -fi - -echo "==> request with 0x-prefixed hex string arg" -R1=$(curl -s 'http://localhost:8008/hello_world?name=0x41424344&num=123') -R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi ABCD 123"},"error":""}' -if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - echo "FAIL" - exit 1 -else - echo "OK" -fi - -echo "==> request with missing params" -R1=$(curl -s 'http://localhost:8008/hello_world') -R2='{"jsonrpc":"2.0","id":"","result":{"Result":"hi 0"},"error":""}' -if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - echo "FAIL" - exit 1 -else - echo "OK" -fi - -echo "==> request with unquoted string arg" -R1=$(curl -s 'http://localhost:8008/hello_world?name=abcd&num=123') -R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: invalid character 'a' looking for beginning of value\"}" -if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - echo "FAIL" - exit 1 -else - echo "OK" -fi - -echo "==> request with string type when expecting number arg" -R1=$(curl -s 'http://localhost:8008/hello_world?name="abcd"&num=0xabcd') -R2="{\"jsonrpc\":\"2.0\",\"id\":\"\",\"result\":null,\"error\":\"Error converting http params to args: Got a hex string arg, but expected 'int'\"}" -if [[ "$R1" != "$R2" ]]; then - echo "responses are not identical:" - echo "R1: $R1" - echo "R2: $R2" - echo "FAIL" - exit 1 -else - echo "OK" -fi - -echo "==> Stopping the server" -kill -9 $PID - -rm -f rpcserver - -popd -exit 0 diff --git a/tm2/pkg/bft/rpc/lib/test/main.go b/tm2/pkg/bft/rpc/lib/test/main.go deleted file mode 100644 index fe7fc49c65e..00000000000 --- a/tm2/pkg/bft/rpc/lib/test/main.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - "os" - - "github.com/gnolang/gno/tm2/pkg/log" - osm "github.com/gnolang/gno/tm2/pkg/os" - - rpcserver "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" - rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" -) - -var routes = map[string]*rpcserver.RPCFunc{ - "hello_world": rpcserver.NewRPCFunc(HelloWorld, "name,num"), -} - -func HelloWorld(ctx *rpctypes.Context, name string, num int) (Result, error) { - return Result{fmt.Sprintf("hi %s %d", name, num)}, nil -} - -type Result struct { - Result string -} - -func main() { - var ( - mux = http.NewServeMux() - logger = log.NewTMLogger(log.NewSyncWriter(os.Stdout)) - ) - - // Stop upon receiving SIGTERM or CTRL-C. - osm.TrapSignal(func() {}) - - rpcserver.RegisterRPCFuncs(mux, routes, logger) - config := rpcserver.DefaultConfig() - listener, err := rpcserver.Listen("0.0.0.0:8008", config) - if err != nil { - osm.Exit(err.Error()) - } - rpcserver.StartHTTPServer(listener, mux, logger, config) -} diff --git a/tm2/pkg/bft/rpc/lib/types/types.go b/tm2/pkg/bft/rpc/lib/types/types.go index 65cafd79fe5..0d58179a0cb 100644 --- a/tm2/pkg/bft/rpc/lib/types/types.go +++ b/tm2/pkg/bft/rpc/lib/types/types.go @@ -12,23 +12,23 @@ import ( "github.com/gnolang/gno/tm2/pkg/errors" ) -// a wrapper to emulate a sum type: jsonrpcid = string | int +// a wrapper to emulate a sum type: JSONRPCID = string | int // TODO: refactor when Go 2.0 arrives https://github.com/golang/go/issues/19412 -type jsonrpcid interface { - isJSONRPCID() +type JSONRPCID interface { + IsJSONRPCID() } // JSONRPCStringID a wrapper for JSON-RPC string IDs type JSONRPCStringID string -func (JSONRPCStringID) isJSONRPCID() {} +func (JSONRPCStringID) IsJSONRPCID() {} // JSONRPCIntID a wrapper for JSON-RPC integer IDs type JSONRPCIntID int -func (JSONRPCIntID) isJSONRPCID() {} +func (JSONRPCIntID) IsJSONRPCID() {} -func idFromInterface(idInterface interface{}) (jsonrpcid, error) { +func idFromInterface(idInterface interface{}) (JSONRPCID, error) { switch id := idInterface.(type) { case string: return JSONRPCStringID(id), nil @@ -49,12 +49,12 @@ func idFromInterface(idInterface interface{}) (jsonrpcid, error) { type RPCRequest struct { JSONRPC string `json:"jsonrpc"` - ID jsonrpcid `json:"id"` + ID JSONRPCID `json:"id"` Method string `json:"method"` Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{} } -// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int +// UnmarshalJSON custom JSON unmarshalling due to JSONRPCID being string or int func (request *RPCRequest) UnmarshalJSON(data []byte) error { unsafeReq := &struct { JSONRPC string `json:"jsonrpc"` @@ -80,7 +80,7 @@ func (request *RPCRequest) UnmarshalJSON(data []byte) error { return nil } -func NewRPCRequest(id jsonrpcid, method string, params json.RawMessage) RPCRequest { +func NewRPCRequest(id JSONRPCID, method string, params json.RawMessage) RPCRequest { return RPCRequest{ JSONRPC: "2.0", ID: id, @@ -93,7 +93,7 @@ func (request RPCRequest) String() string { return fmt.Sprintf("[%s %s]", request.ID, request.Method) } -func MapToRequest(id jsonrpcid, method string, params map[string]interface{}) (RPCRequest, error) { +func MapToRequest(id JSONRPCID, method string, params map[string]interface{}) (RPCRequest, error) { params_ := make(map[string]json.RawMessage, len(params)) for name, value := range params { valueJSON, err := amino.MarshalJSON(value) @@ -110,7 +110,7 @@ func MapToRequest(id jsonrpcid, method string, params map[string]interface{}) (R return request, nil } -func ArrayToRequest(id jsonrpcid, method string, params []interface{}) (RPCRequest, error) { +func ArrayToRequest(id JSONRPCID, method string, params []interface{}) (RPCRequest, error) { params_ := make([]json.RawMessage, len(params)) for i, value := range params { valueJSON, err := amino.MarshalJSON(value) @@ -146,12 +146,17 @@ func (err RPCError) Error() string { type RPCResponse struct { JSONRPC string `json:"jsonrpc"` - ID jsonrpcid `json:"id"` + ID JSONRPCID `json:"id"` Result json.RawMessage `json:"result,omitempty"` Error *RPCError `json:"error,omitempty"` } -// UnmarshalJSON custom JSON unmarshalling due to jsonrpcid being string or int +type ( + RPCRequests []RPCRequest + RPCResponses []RPCResponse +) + +// UnmarshalJSON custom JSON unmarshalling due to JSONRPCID being string or int func (response *RPCResponse) UnmarshalJSON(data []byte) error { unsafeResp := &struct { JSONRPC string `json:"jsonrpc"` @@ -177,7 +182,7 @@ func (response *RPCResponse) UnmarshalJSON(data []byte) error { return nil } -func NewRPCSuccessResponse(id jsonrpcid, res interface{}) RPCResponse { +func NewRPCSuccessResponse(id JSONRPCID, res interface{}) RPCResponse { var rawMsg json.RawMessage if res != nil { @@ -192,7 +197,7 @@ func NewRPCSuccessResponse(id jsonrpcid, res interface{}) RPCResponse { return RPCResponse{JSONRPC: "2.0", ID: id, Result: rawMsg} } -func NewRPCErrorResponse(id jsonrpcid, code int, msg string, data string) RPCResponse { +func NewRPCErrorResponse(id JSONRPCID, code int, msg string, data string) RPCResponse { return RPCResponse{ JSONRPC: "2.0", ID: id, @@ -207,27 +212,27 @@ func (response RPCResponse) String() string { return fmt.Sprintf("[%s %s]", response.ID, response.Error) } -func RPCParseError(id jsonrpcid, err error) RPCResponse { +func RPCParseError(id JSONRPCID, err error) RPCResponse { return NewRPCErrorResponse(id, -32700, "Parse error. Invalid JSON", err.Error()) } -func RPCInvalidRequestError(id jsonrpcid, err error) RPCResponse { +func RPCInvalidRequestError(id JSONRPCID, err error) RPCResponse { return NewRPCErrorResponse(id, -32600, "Invalid Request", err.Error()) } -func RPCMethodNotFoundError(id jsonrpcid) RPCResponse { +func RPCMethodNotFoundError(id JSONRPCID) RPCResponse { return NewRPCErrorResponse(id, -32601, "Method not found", "") } -func RPCInvalidParamsError(id jsonrpcid, err error) RPCResponse { +func RPCInvalidParamsError(id JSONRPCID, err error) RPCResponse { return NewRPCErrorResponse(id, -32602, "Invalid params", err.Error()) } -func RPCInternalError(id jsonrpcid, err error) RPCResponse { +func RPCInternalError(id JSONRPCID, err error) RPCResponse { return NewRPCErrorResponse(id, -32603, "Internal error", err.Error()) } -func RPCServerError(id jsonrpcid, err error) RPCResponse { +func RPCServerError(id JSONRPCID, err error) RPCResponse { return NewRPCErrorResponse(id, -32000, "Server error", err.Error()) } diff --git a/tm2/pkg/bft/rpc/lib/types/types_test.go b/tm2/pkg/bft/rpc/lib/types/types_test.go index 55ee8ed3945..367ec83bfa3 100644 --- a/tm2/pkg/bft/rpc/lib/types/types_test.go +++ b/tm2/pkg/bft/rpc/lib/types/types_test.go @@ -15,7 +15,7 @@ type SampleResult struct { } type responseTest struct { - id jsonrpcid + id JSONRPCID expected string } diff --git a/tm2/pkg/bft/rpc/test/helpers.go b/tm2/pkg/bft/rpc/test/helpers.go index d8e1a498600..2e31fb0c75f 100644 --- a/tm2/pkg/bft/rpc/test/helpers.go +++ b/tm2/pkg/bft/rpc/test/helpers.go @@ -38,7 +38,7 @@ func waitForRPC() { client := rpcclient.NewJSONRPCClient(laddr) result := new(ctypes.ResultStatus) for { - _, err := client.Call("status", map[string]interface{}{}, result) + err := client.Call("status", map[string]interface{}{}, result) if err == nil { return } else { diff --git a/tm2/pkg/crypto/keys/client/broadcast.go b/tm2/pkg/crypto/keys/client/broadcast.go index 9c05f2c43b3..6be60047a77 100644 --- a/tm2/pkg/crypto/keys/client/broadcast.go +++ b/tm2/pkg/crypto/keys/client/broadcast.go @@ -100,7 +100,7 @@ func broadcastHandler(cfg *broadcastCfg) (*ctypes.ResultBroadcastTxCommit, error return nil, errors.Wrap(err, "remarshaling tx binary bytes") } - cli := client.NewHTTP(remote, "/websocket") + cli := client.NewHTTP(remote) if cfg.dryRun { return simulateTx(cli, bz) diff --git a/tm2/pkg/crypto/keys/client/query.go b/tm2/pkg/crypto/keys/client/query.go index 746048e772c..d5d2ffc166a 100644 --- a/tm2/pkg/crypto/keys/client/query.go +++ b/tm2/pkg/crypto/keys/client/query.go @@ -101,7 +101,7 @@ func queryHandler(cfg *queryCfg) (*ctypes.ResultABCIQuery, error) { // Height: height, XXX // Prove: false, XXX } - cli := client.NewHTTP(remote, "/websocket") + cli := client.NewHTTP(remote) qres, err := cli.ABCIQueryWithOptions( cfg.path, data, opts2) if err != nil { From 37d46507d61eec39b538a81e141db1dd6f60fe12 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 5 Jan 2024 23:08:11 +0100 Subject: [PATCH 03/26] Replicate batch processing for the ws server --- tm2/pkg/bft/rpc/lib/server/handlers.go | 114 ++++++++++++++++--------- tm2/pkg/bft/rpc/lib/types/types.go | 8 +- 2 files changed, 76 insertions(+), 46 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index 7243d2ee4de..f26de5d20b2 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -434,7 +434,7 @@ type wsConnection struct { remoteAddr string baseConn *websocket.Conn - writeChan chan types.RPCResponse + writeChan chan types.RPCResponses funcMap map[string]*RPCFunc @@ -539,7 +539,7 @@ func ReadLimit(readLimit int64) func(*wsConnection) { // OnStart implements service.Service by starting the read and write routines. It // blocks until the connection closes. func (wsc *wsConnection) OnStart() error { - wsc.writeChan = make(chan types.RPCResponse, wsc.writeChanCapacity) + wsc.writeChan = make(chan types.RPCResponses, wsc.writeChanCapacity) // Read subscriptions/unsubscriptions to events go wsc.readRoutine() @@ -552,7 +552,7 @@ func (wsc *wsConnection) OnStart() error { // OnStop implements service.Service by unsubscribing remoteAddr from all subscriptions. func (wsc *wsConnection) OnStop() { // Both read and write loops close the websocket connection when they exit their loops. - // The writeChan is never closed, to allow WriteRPCResponse() to fail. + // The writeChan is never closed, to allow WriteRPCResponses() to fail. if wsc.onDisconnect != nil { wsc.onDisconnect(wsc.remoteAddr) @@ -571,7 +571,7 @@ func (wsc *wsConnection) GetRemoteAddr() string { // WriteRPCResponse pushes a response to the writeChan, and blocks until it is accepted. // It implements WSRPCConnection. It is Goroutine-safe. -func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { +func (wsc *wsConnection) WriteRPCResponses(resp types.RPCResponses) { select { case <-wsc.Quit(): return @@ -581,7 +581,7 @@ func (wsc *wsConnection) WriteRPCResponse(resp types.RPCResponse) { // TryWriteRPCResponse attempts to push a response to the writeChan, but does not block. // It implements WSRPCConnection. It is Goroutine-safe -func (wsc *wsConnection) TryWriteRPCResponse(resp types.RPCResponse) bool { +func (wsc *wsConnection) TryWriteRPCResponses(resp types.RPCResponses) bool { select { case <-wsc.Quit(): return false @@ -611,7 +611,7 @@ func (wsc *wsConnection) readRoutine() { err = fmt.Errorf("WSJSONRPC: %v", r) } wsc.Logger.Error("Panic in WSJSONRPC handler", "err", err, "stack", string(debug.Stack())) - wsc.WriteRPCResponse(types.RPCInternalError(types.JSONRPCStringID("unknown"), err)) + wsc.WriteRPCResponses(types.RPCResponses{types.RPCInternalError(types.JSONRPCStringID("unknown"), err)}) go wsc.readRoutine() } else { wsc.baseConn.Close() //nolint: errcheck @@ -643,50 +643,72 @@ func (wsc *wsConnection) readRoutine() { return } - var request types.RPCRequest - err = json.Unmarshal(in, &request) - if err != nil { - wsc.WriteRPCResponse(types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))) - continue - } + // first try to unmarshal the incoming request as an array of RPC requests + var ( + requests types.RPCRequests + responses types.RPCResponses + ) - // 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("") { - wsc.Logger.Debug("WSJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") - continue - } + if err := json.Unmarshal(in, &requests); err != nil { + // next, try to unmarshal as a single request + var request types.RPCRequest + if err := json.Unmarshal(in, &request); err != nil { + wsc.WriteRPCResponses(types.RPCResponses{types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))}) - // Now, fetch the RPCFunc and execute it. - rpcFunc := wsc.funcMap[request.Method] - if rpcFunc == nil { - wsc.WriteRPCResponse(types.RPCMethodNotFoundError(request.ID)) - continue + return + } + + requests = []types.RPCRequest{request} } - ctx := &types.Context{JSONReq: &request, WSConn: wsc} - args := []reflect.Value{reflect.ValueOf(ctx)} - if len(request.Params) > 0 { - fnArgs, err := jsonParamsToArgs(rpcFunc, request.Params) - if err != nil { - wsc.WriteRPCResponse(types.RPCInternalError(request.ID, errors.Wrap(err, "error converting json params to arguments"))) + for _, request := range requests { + request := request + + // 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("") { + wsc.Logger.Debug("WSJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") continue } - args = append(args, fnArgs...) - } - returns := rpcFunc.f.Call(args) + // Now, fetch the RPCFunc and execute it. + rpcFunc := wsc.funcMap[request.Method] + if rpcFunc == nil { + responses = append(responses, types.RPCMethodNotFoundError(request.ID)) - // TODO: Need to encode args/returns to string if we want to log them - wsc.Logger.Info("WSJSONRPC", "method", request.Method) + continue + } - result, err := unreflectResult(returns) - if err != nil { - wsc.WriteRPCResponse(types.RPCInternalError(request.ID, err)) - continue - } + ctx := &types.Context{JSONReq: &request, WSConn: wsc} + args := []reflect.Value{reflect.ValueOf(ctx)} + if len(request.Params) > 0 { + fnArgs, err := jsonParamsToArgs(rpcFunc, request.Params) + if err != nil { + responses = append(responses, types.RPCInternalError(request.ID, errors.Wrap(err, "error converting json params to arguments"))) + + continue + } + args = append(args, fnArgs...) + } + + returns := rpcFunc.f.Call(args) + + // TODO: Need to encode args/returns to string if we want to log them + wsc.Logger.Info("WSJSONRPC", "method", request.Method) + + result, err := unreflectResult(returns) + if err != nil { + responses = append(responses, types.RPCInternalError(request.ID, err)) + + continue + } + + responses = append(responses, types.NewRPCSuccessResponse(request.ID, result)) - wsc.WriteRPCResponse(types.NewRPCSuccessResponse(request.ID, result)) + if len(responses) > 0 { + wsc.WriteRPCResponses(responses) + } + } } } } @@ -725,8 +747,16 @@ func (wsc *wsConnection) writeRoutine() { wsc.Stop() return } - case msg := <-wsc.writeChan: - jsonBytes, err := json.MarshalIndent(msg, "", " ") + case msgs := <-wsc.writeChan: + var writeData any + + if len(msgs) == 1 { + writeData = msgs[0] + } else { + writeData = msgs + } + + jsonBytes, err := json.MarshalIndent(writeData, "", " ") if err != nil { wsc.Logger.Error("Failed to marshal RPCResponse to JSON", "err", err) } else if err = wsc.writeMessageWithDeadline(websocket.TextMessage, jsonBytes); err != nil { diff --git a/tm2/pkg/bft/rpc/lib/types/types.go b/tm2/pkg/bft/rpc/lib/types/types.go index 0d58179a0cb..43afa3a9811 100644 --- a/tm2/pkg/bft/rpc/lib/types/types.go +++ b/tm2/pkg/bft/rpc/lib/types/types.go @@ -242,10 +242,10 @@ func RPCServerError(id JSONRPCID, err error) RPCResponse { type WSRPCConnection interface { // GetRemoteAddr returns a remote address of the connection. GetRemoteAddr() string - // WriteRPCResponse writes the resp onto connection (BLOCKING). - WriteRPCResponse(resp RPCResponse) - // TryWriteRPCResponse tries to write the resp onto connection (NON-BLOCKING). - TryWriteRPCResponse(resp RPCResponse) bool + // WriteRPCResponses writes the resp onto connection (BLOCKING). + WriteRPCResponses(resp RPCResponses) + // TryWriteRPCResponses tries to write the resp onto connection (NON-BLOCKING). + TryWriteRPCResponses(resp RPCResponses) bool // Context returns the connection's context. Context() context.Context } From 9bda10bc346a7247bb89916478752b9738434fdc Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 8 Apr 2024 20:27:18 +0200 Subject: [PATCH 04/26] Start chipping away at the WS client --- tm2/pkg/bft/rpc/client/ws.go | 40 +- tm2/pkg/bft/rpc/lib/client/batch.go | 12 +- tm2/pkg/bft/rpc/lib/client/http_client.go | 12 +- .../bft/rpc/lib/client/integration_test.go | 5 +- tm2/pkg/bft/rpc/lib/client/ws/client.go | 240 ++++++++ tm2/pkg/bft/rpc/lib/client/ws/client_test.go | 1 + tm2/pkg/bft/rpc/lib/client/ws/options.go | 14 + tm2/pkg/bft/rpc/lib/client/ws_client.go | 569 ------------------ tm2/pkg/bft/rpc/lib/client/ws_client_test.go | 264 -------- tm2/pkg/bft/rpc/lib/rpc_test.go | 248 +------- tm2/pkg/bft/rpc/lib/types/types.go | 43 +- 11 files changed, 303 insertions(+), 1145 deletions(-) create mode 100644 tm2/pkg/bft/rpc/lib/client/ws/client.go create mode 100644 tm2/pkg/bft/rpc/lib/client/ws/client_test.go create mode 100644 tm2/pkg/bft/rpc/lib/client/ws/options.go delete mode 100644 tm2/pkg/bft/rpc/lib/client/ws_client.go delete mode 100644 tm2/pkg/bft/rpc/lib/client/ws_client_test.go diff --git a/tm2/pkg/bft/rpc/client/ws.go b/tm2/pkg/bft/rpc/client/ws.go index a3514f851cd..300c69d8b27 100644 --- a/tm2/pkg/bft/rpc/client/ws.go +++ b/tm2/pkg/bft/rpc/client/ws.go @@ -1,29 +1,31 @@ package client -import rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" +import ( + "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/ws" +) var _ Client = (*WS)(nil) type WS struct { - rpc *rpcclient.WSClient + rpc *ws.Client *baseRPCClient } -func NewWS(remote, endpoint string) *WS { - return &WS{ - rpc: rpcclient.NewWSClient(remote, endpoint), - } -} - -// NewBatch creates a new rpcBatch client for this HTTP client. -func (c *WS) NewBatch() *Batch { - batch := rpcclient.NewRPCRequestBatch(c.rpc) - - return &Batch{ - rpcBatch: batch, - baseRPCClient: &baseRPCClient{ - caller: batch, - }, - } -} +// func NewWS(remote, endpoint string) *WS { +// return &WS{ +// rpc: ws.NewClient(remote, endpoint), +// } +// } +// +// // NewBatch creates a new rpcBatch client for this HTTP client. +// func (c *WS) NewBatch() *Batch { +// batch := rpcclient.NewRPCRequestBatch(c.rpc) +// +// return &Batch{ +// rpcBatch: batch, +// baseRPCClient: &baseRPCClient{ +// caller: batch, +// }, +// } +// } diff --git a/tm2/pkg/bft/rpc/lib/client/batch.go b/tm2/pkg/bft/rpc/lib/client/batch.go index ea734ffede9..662413ee874 100644 --- a/tm2/pkg/bft/rpc/lib/client/batch.go +++ b/tm2/pkg/bft/rpc/lib/client/batch.go @@ -10,7 +10,7 @@ import ( ) type BatchClient interface { - SendBatch(ctx context.Context, requests wrappedRPCRequests) (types.RPCResponses, error) + SendBatch(ctx context.Context, requests WrappedRPCRequests) (types.RPCResponses, error) GetIDPrefix() types.JSONRPCID } @@ -23,14 +23,14 @@ type RPCRequestBatch struct { sync.Mutex client BatchClient - requests wrappedRPCRequests + requests WrappedRPCRequests } // NewRPCRequestBatch creates a new func NewRPCRequestBatch(client BatchClient) *RPCRequestBatch { return &RPCRequestBatch{ client: client, - requests: make(wrappedRPCRequests, 0), + requests: make(WrappedRPCRequests, 0), } } @@ -52,7 +52,7 @@ func (b *RPCRequestBatch) Clear() int { func (b *RPCRequestBatch) clear() int { count := len(b.requests) - b.requests = make(wrappedRPCRequests, 0) + b.requests = make(WrappedRPCRequests, 0) return count } @@ -101,7 +101,7 @@ func (b *RPCRequestBatch) Call(method string, params map[string]any, result any) } b.enqueue( - &wrappedRPCRequest{ + &WrappedRPCRequest{ request: request, result: result, }, @@ -110,7 +110,7 @@ func (b *RPCRequestBatch) Call(method string, params map[string]any, result any) return nil } -func (b *RPCRequestBatch) enqueue(req *wrappedRPCRequest) { +func (b *RPCRequestBatch) enqueue(req *WrappedRPCRequest) { b.Lock() defer b.Unlock() diff --git a/tm2/pkg/bft/rpc/lib/client/http_client.go b/tm2/pkg/bft/rpc/lib/client/http_client.go index 6bdf6598977..0f6f2dca9d2 100644 --- a/tm2/pkg/bft/rpc/lib/client/http_client.go +++ b/tm2/pkg/bft/rpc/lib/client/http_client.go @@ -131,20 +131,20 @@ type RPCCaller interface { Call(method string, params map[string]any, result any) error } -// wrappedRPCRequest encapsulates a single buffered request, as well as its +// WrappedRPCRequest encapsulates a single buffered request, as well as its // anticipated response structure -type wrappedRPCRequest struct { +type WrappedRPCRequest struct { request types.RPCRequest result any // The result will be deserialized into this object (Amino) } -type wrappedRPCRequests []*wrappedRPCRequest +type WrappedRPCRequests []*WrappedRPCRequest -func (w *wrappedRPCRequest) extractRPCRequest() types.RPCRequest { +func (w *WrappedRPCRequest) extractRPCRequest() types.RPCRequest { return w.request } -func (w *wrappedRPCRequests) extractRPCRequests() types.RPCRequests { +func (w *WrappedRPCRequests) extractRPCRequests() types.RPCRequests { requests := make([]types.RPCRequest, 0, len(*w)) for _, wrappedRequest := range *w { @@ -226,7 +226,7 @@ func (c *JSONRPCClient) Call(method string, params map[string]any, result any) e return unmarshalResponseIntoResult(&response, id, result) } -func (c *JSONRPCClient) SendBatch(_ context.Context, wrappedRequests wrappedRPCRequests) (types.RPCResponses, error) { +func (c *JSONRPCClient) SendBatch(_ context.Context, wrappedRequests WrappedRPCRequests) (types.RPCResponses, error) { requests := make(types.RPCRequests, 0, len(wrappedRequests)) for _, request := range wrappedRequests { requests = append(requests, request.request) diff --git a/tm2/pkg/bft/rpc/lib/client/integration_test.go b/tm2/pkg/bft/rpc/lib/client/integration_test.go index f3c705ecf98..0b0bdc1778f 100644 --- a/tm2/pkg/bft/rpc/lib/client/integration_test.go +++ b/tm2/pkg/bft/rpc/lib/client/integration_test.go @@ -13,6 +13,7 @@ import ( "testing" "time" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/ws" "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/log" @@ -27,11 +28,11 @@ func TestWSClientReconnectWithJitter(t *testing.T) { maxSleepTime := time.Second * time.Duration(((1< server write routine +func (c *Client) runWriteRoutine(ctx context.Context) { + for { + select { + case <-ctx.Done(): + c.logger.Debug("write context finished") + + return + case item := <-c.backlog: + // Write the JSON request to the server + if err := c.conn.WriteJSON(item); err != nil { + c.logger.Error("unable to send request", "err", err) + + continue + } + + c.logger.Debug("successfully sent request", "request", item) + } + } +} + +// runReadRoutine runs the client <- server read routine +func (c *Client) runReadRoutine(ctx context.Context) { + for { + select { + case <-ctx.Done(): + c.logger.Debug("read context finished") + + return + default: + // Read the message from the active connection + _, data, err := c.conn.ReadMessage() // TODO check message type + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { + c.logger.Error("failed to read response", "err", err) + + return + } + + continue + } + + var ( + responses types.RPCResponses + responseHash string + ) + + // Try to unmarshal as a batch of responses first + if err := json.Unmarshal(data, &responses); err != nil { + // Try to unmarshal as a single response + var response types.RPCResponse + + if err := json.Unmarshal(data, &response); err != nil { + c.logger.Error("failed to parse response", "err", err, "data", string(data)) + + continue + } + + // This is a single response, generate the unique ID + responseHash = generateIDHash(response.ID.String()) + responses = types.RPCResponses{response} + } else { + // This is a batch response, generate the unique ID + // from the combined IDs + ids := make([]string, 0, len(responses)) + + for _, response := range responses { + ids = append(ids, response.ID.String()) + } + + responseHash = generateIDHash(ids...) + } + + // Grab the response channel + c.requestMapMux.Lock() + ch := c.requestMap[responseHash] + if ch == nil { + c.requestMapMux.Unlock() + c.logger.Error("response listener not set", "hash", responseHash, "responses", responses) + + continue + } + + // Clear the entry for this ID + delete(c.requestMap, responseHash) + c.requestMapMux.Unlock() + + c.logger.Debug("received response", "hash", responseHash) + + // Alert the listener of the response + select { + case ch <- responses: + default: + c.logger.Warn("response listener timed out", "hash", responseHash) + } + } + } +} + +// Close closes the WS client +func (c *Client) Close() { + c.cancelFn() +} diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client_test.go b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go new file mode 100644 index 00000000000..98592950df4 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go @@ -0,0 +1 @@ +package ws diff --git a/tm2/pkg/bft/rpc/lib/client/ws/options.go b/tm2/pkg/bft/rpc/lib/client/ws/options.go new file mode 100644 index 00000000000..c98e8923b22 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/ws/options.go @@ -0,0 +1,14 @@ +package ws + +import ( + "log/slog" +) + +type Option func(*Client) + +// WithLogger sets the WS client logger +func WithLogger(logger *slog.Logger) Option { + return func(c *Client) { + c.logger = logger + } +} diff --git a/tm2/pkg/bft/rpc/lib/client/ws_client.go b/tm2/pkg/bft/rpc/lib/client/ws_client.go deleted file mode 100644 index 0a9b43cc9b0..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/ws_client.go +++ /dev/null @@ -1,569 +0,0 @@ -package rpcclient - -import ( - "context" - "encoding/json" - "fmt" - "net" - "net/http" - "sync" - "time" - - types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/gno/tm2/pkg/random" - "github.com/gnolang/gno/tm2/pkg/service" - "github.com/gorilla/websocket" -) - -const ( - defaultMaxReconnectAttempts = 25 - defaultWriteWait = 0 - defaultReadWait = 0 - defaultPingPeriod = 0 -) - -var _ BatchClient = (*WSClient)(nil) - -// WSClient is a WebSocket client. The methods of WSClient are safe for use by -// multiple goroutines. -type WSClient struct { - service.BaseService - - conn *websocket.Conn - - Address string // IP:PORT or /path/to/socket - Endpoint string // /websocket/url/endpoint - Dialer func(string, string) (net.Conn, error) - - // Single user facing channel to read RPCResponses from, closed only when the client is being stopped. - ResponsesCh chan types.RPCResponses - - // Callback, which will be called each time after successful reconnect. - onReconnect func() - - // internal channels - send chan wrappedRPCRequests // user requests - backlog chan wrappedRPCRequests // stores user requests received during a conn failure - reconnectAfter chan error // reconnect requests - readRoutineQuit chan struct{} // a way for readRoutine to close writeRoutine - - wg sync.WaitGroup - - mtx sync.RWMutex - sentLastPingAt time.Time - reconnecting bool - - // Maximum reconnect attempts (0 or greater; default: 25). - maxReconnectAttempts int - - // Time allowed to write a message to the server. 0 means block until operation succeeds. - writeWait time.Duration - - // Time allowed to read the next message from the server. 0 means block until operation succeeds. - readWait time.Duration - - // Send pings to server with this period. Must be less than readWait. If 0, no pings will be sent. - pingPeriod time.Duration - - // Support both ws and wss protocols - protocol string - - idPrefix types.JSONRPCID - - // Since requests are sent in, and responses - // are parsed asynchronously in this implementation, - // the thread that is parsing the response result - // needs to perform Amino decoding. - // The thread that spawn the request, and the thread that parses - // the request do not live in the same context. - // This information (the object type) needs to be available at this moment, - // so a map is used to temporarily store those types - requestResultsMap map[types.JSONRPCID]any -} - -func (c *WSClient) SendBatch(ctx context.Context, requests wrappedRPCRequests) (types.RPCResponses, error) { - if err := c.Send(ctx, requests...); err != nil { - return nil, err - } - - select { - case <-ctx.Done(): - return nil, errors.New("timed out") - case responses := <-c.ResponsesCh: - return responses, nil - } -} - -// NewWSClient returns a new client. See the commentary on the func(*WSClient) -// functions for a detailed description of how to configure ping period and -// pong wait time. The endpoint argument must begin with a `/`. -// The function panics if the provided address is invalid. -func NewWSClient(remoteAddr, endpoint string, options ...func(*WSClient)) *WSClient { - protocol, addr, err := toClientAddrAndParse(remoteAddr) - if err != nil { - panic(fmt.Sprintf("invalid remote %s: %s", remoteAddr, err)) - } - // default to ws protocol, unless wss is explicitly specified - if protocol != "wss" { - protocol = "ws" - } - - c := &WSClient{ - Address: addr, - Dialer: makeHTTPDialer(remoteAddr), - Endpoint: endpoint, - - maxReconnectAttempts: defaultMaxReconnectAttempts, - readWait: defaultReadWait, - writeWait: defaultWriteWait, - pingPeriod: defaultPingPeriod, - protocol: protocol, - idPrefix: types.JSONRPCStringID("ws-client-" + random.RandStr(8)), - requestResultsMap: make(map[types.JSONRPCID]any), - } - c.BaseService = *service.NewBaseService(nil, "WSClient", c) - for _, option := range options { - option(c) - } - return c -} - -// MaxReconnectAttempts sets the maximum number of reconnect attempts before returning an error. -// It should only be used in the constructor and is not Goroutine-safe. -func MaxReconnectAttempts(max int) func(*WSClient) { - return func(c *WSClient) { - c.maxReconnectAttempts = max - } -} - -// ReadWait sets the amount of time to wait before a websocket read times out. -// It should only be used in the constructor and is not Goroutine-safe. -func ReadWait(readWait time.Duration) func(*WSClient) { - return func(c *WSClient) { - c.readWait = readWait - } -} - -// WriteWait sets the amount of time to wait before a websocket write times out. -// It should only be used in the constructor and is not Goroutine-safe. -func WriteWait(writeWait time.Duration) func(*WSClient) { - return func(c *WSClient) { - c.writeWait = writeWait - } -} - -// PingPeriod sets the duration for sending websocket pings. -// It should only be used in the constructor - not Goroutine-safe. -func PingPeriod(pingPeriod time.Duration) func(*WSClient) { - return func(c *WSClient) { - c.pingPeriod = pingPeriod - } -} - -// OnReconnect sets the callback, which will be called every time after -// successful reconnect. -func OnReconnect(cb func()) func(*WSClient) { - return func(c *WSClient) { - c.onReconnect = cb - } -} - -// String returns WS client full address. -func (c *WSClient) String() string { - return fmt.Sprintf("%s (%s)", c.Address, c.Endpoint) -} - -func (c *WSClient) GetIDPrefix() types.JSONRPCID { - return c.idPrefix -} - -// OnStart implements service.Service by dialing a server and creating read and -// write routines. -func (c *WSClient) OnStart() error { - err := c.dial() - if err != nil { - return err - } - - c.ResponsesCh = make(chan types.RPCResponses) - - c.send = make(chan wrappedRPCRequests) - // 1 additional error may come from the read/write - // goroutine depending on which failed first. - c.reconnectAfter = make(chan error, 1) - // capacity for 1 request. a user won't be able to send more because the send - // channel is unbuffered. - c.backlog = make(chan wrappedRPCRequests, 1) - - c.startReadWriteRoutines() - go c.reconnectRoutine() - - return nil -} - -// Stop overrides service.Service#Stop. There is no other way to wait until Quit -// channel is closed. -func (c *WSClient) Stop() error { - if err := c.BaseService.Stop(); err != nil { - return err - } - // only close user-facing channels when we can't write to them - c.wg.Wait() - close(c.ResponsesCh) - - return nil -} - -// IsReconnecting returns true if the client is reconnecting right now. -func (c *WSClient) IsReconnecting() bool { - c.mtx.RLock() - defer c.mtx.RUnlock() - return c.reconnecting -} - -// IsActive returns true if the client is running and not reconnecting. -func (c *WSClient) IsActive() bool { - return c.IsRunning() && !c.IsReconnecting() -} - -// Send the given RPC request to the server. Results will be available on -// ResponsesCh, errors, if any, on ErrorsCh. Will block until send succeeds or -// ctx.Done is closed. -func (c *WSClient) Send(ctx context.Context, requests ...*wrappedRPCRequest) error { - select { - case c.send <- requests: - c.Logger.Info("sent requests", "num", len(requests)) - return nil - case <-ctx.Done(): - return ctx.Err() - } -} - -// Call the given method. See Send description. -func (c *WSClient) Call(ctx context.Context, method string, params map[string]any, result any) error { - id := generateRequestID(c.idPrefix) - - request, err := types.MapToRequest(id, method, params) - if err != nil { - return err - } - - return c.Send( - ctx, - &wrappedRPCRequest{ - request: request, - result: result, - }, - ) -} - -// CallWithArrayParams the given method with params in a form of array. See -// Send description. -func (c *WSClient) CallWithArrayParams(ctx context.Context, method string, params []any, result any) error { - id := generateRequestID(c.idPrefix) - - request, err := types.ArrayToRequest(id, method, params) - if err != nil { - return err - } - - return c.Send( - ctx, - &wrappedRPCRequest{ - request: request, - result: result, - }, - ) -} - -// ----------- -// Private methods - -func (c *WSClient) dial() error { - dialer := &websocket.Dialer{ - NetDial: c.Dialer, - Proxy: http.ProxyFromEnvironment, - } - rHeader := http.Header{} - conn, _, err := dialer.Dial(c.protocol+"://"+c.Address+c.Endpoint, rHeader) - if err != nil { - return err - } - c.conn = conn - return nil -} - -// reconnect tries to redial up to maxReconnectAttempts with exponential -// backoff. -func (c *WSClient) reconnect() error { - attempt := 0 - - c.mtx.Lock() - c.reconnecting = true - c.mtx.Unlock() - defer func() { - c.mtx.Lock() - c.reconnecting = false - c.mtx.Unlock() - }() - - for { - jitter := time.Duration(random.RandFloat64() * float64(time.Second)) // 1s == (1e9 ns) - backoffDuration := jitter + ((1 << uint(attempt)) * time.Second) - - c.Logger.Info("reconnecting", "attempt", attempt+1, "backoff_duration", backoffDuration) - time.Sleep(backoffDuration) - - err := c.dial() - if err != nil { - c.Logger.Error("failed to redial", "err", err) - } else { - c.Logger.Info("reconnected") - if c.onReconnect != nil { - go c.onReconnect() - } - return nil - } - - attempt++ - - if attempt > c.maxReconnectAttempts { - return errors.Wrap(err, "reached maximum reconnect attempts") - } - } -} - -func (c *WSClient) startReadWriteRoutines() { - c.wg.Add(2) - c.readRoutineQuit = make(chan struct{}) - go c.readRoutine() - go c.writeRoutine() -} - -func (c *WSClient) processBacklog() error { - select { - case wrappedRequests := <-c.backlog: - if c.writeWait > 0 { - if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { - c.Logger.Error("failed to set write deadline", "err", err) - } - } - - writeData := extractWriteData(wrappedRequests) - - if err := c.conn.WriteJSON(writeData); err != nil { - c.Logger.Error("failed to resend request", "err", err) - c.reconnectAfter <- err - // requeue request - c.backlog <- wrappedRequests - return err - } - - c.Logger.Info("resend a request", "req", writeData) - default: - } - return nil -} - -func (c *WSClient) reconnectRoutine() { - for { - select { - case originalError := <-c.reconnectAfter: - // wait until writeRoutine and readRoutine finish - c.wg.Wait() - if err := c.reconnect(); err != nil { - c.Logger.Error("failed to reconnect", "err", err, "original_err", originalError) - c.Stop() - return - } - // drain reconnectAfter - LOOP: - for { - select { - case <-c.reconnectAfter: - default: - break LOOP - } - } - err := c.processBacklog() - if err == nil { - c.startReadWriteRoutines() - } - - case <-c.Quit(): - return - } - } -} - -// The client ensures that there is at most one writer to a connection by -// executing all writes from this goroutine. -func (c *WSClient) writeRoutine() { - var ticker *time.Ticker - if c.pingPeriod > 0 { - // ticker with a predefined period - ticker = time.NewTicker(c.pingPeriod) - } else { - // ticker that never fires - ticker = &time.Ticker{C: make(<-chan time.Time)} - } - - defer func() { - ticker.Stop() - c.conn.Close() - // err != nil { - // ignore error; it will trigger in tests - // likely because it's closing an already closed connection - // } - c.wg.Done() - }() - - for { - select { - case wrappedRequests := <-c.send: - if c.writeWait > 0 { - if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { - c.Logger.Error("failed to set write deadline", "err", err) - } - } - - // Save the results for later lookups - for _, req := range wrappedRequests { - c.requestResultsMap[req.request.ID] = req.result - } - - writeData := extractWriteData(wrappedRequests) - - if err := c.conn.WriteJSON(writeData); err != nil { - c.Logger.Error("failed to send request", "err", err) - c.reconnectAfter <- err - // add request to the backlog, so we don't lose it - c.backlog <- wrappedRequests - return - } - case <-ticker.C: - if c.writeWait > 0 { - if err := c.conn.SetWriteDeadline(time.Now().Add(c.writeWait)); err != nil { - c.Logger.Error("failed to set write deadline", "err", err) - } - } - if err := c.conn.WriteMessage(websocket.PingMessage, []byte{}); err != nil { - c.Logger.Error("failed to write ping", "err", err) - c.reconnectAfter <- err - return - } - c.mtx.Lock() - c.sentLastPingAt = time.Now() - c.mtx.Unlock() - c.Logger.Debug("sent ping") - case <-c.readRoutineQuit: - return - case <-c.Quit(): - if err := c.conn.WriteMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil { - c.Logger.Error("failed to write message", "err", err) - } - return - } - } -} - -// The client ensures that there is at most one reader to a connection by -// executing all reads from this goroutine. -func (c *WSClient) readRoutine() { - defer func() { - c.conn.Close() - // err != nil { - // ignore error; it will trigger in tests - // likely because it's closing an already closed connection - // } - c.wg.Done() - }() - - c.conn.SetPongHandler(func(string) error { - /* - TODO latency metrics - // gather latency stats - c.mtx.RLock() - t := c.sentLastPingAt - c.mtx.RUnlock() - */ - - c.Logger.Debug("got pong") - return nil - }) - - for { - // reset deadline for every message type (control or data) - if c.readWait > 0 { - if err := c.conn.SetReadDeadline(time.Now().Add(c.readWait)); err != nil { - c.Logger.Error("failed to set read deadline", "err", err) - } - } - _, data, err := c.conn.ReadMessage() - if err != nil { - if !websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { - return - } - - c.Logger.Error("failed to read response", "err", err) - close(c.readRoutineQuit) - c.reconnectAfter <- err - return - } - - var responses types.RPCResponses - - // Try to unmarshal as a batch of responses first - if err := json.Unmarshal(data, &responses); err != nil { - // Try to unmarshal as a single response - var response types.RPCResponse - - if err := json.Unmarshal(data, &response); err != nil { - c.Logger.Error("failed to parse response", "err", err, "data", string(data)) - - continue - } - - result, ok := c.requestResultsMap[response.ID] - if !ok { - c.Logger.Error("response result not set", "id", response.ID, "response", response) - - continue - } - - // Clear the expected result for this ID - delete(c.requestResultsMap, response.ID) - - if err := unmarshalResponseIntoResult(&response, response.ID, result); err != nil { - c.Logger.Error("failed to parse response", "err", err, "data", string(data)) - - continue - } - - responses = types.RPCResponses{response} - } - - for _, userResponse := range responses { - c.Logger.Info("got response", "resp", userResponse.Result) - } - - // Combine a non-blocking read on BaseService.Quit with a non-blocking write on ResponsesCh to avoid blocking - // c.wg.Wait() in c.Stop(). Note we rely on Quit being closed so that it sends unlimited Quit signals to stop - // both readRoutine and writeRoutine - select { - case <-c.Quit(): - case c.ResponsesCh <- responses: - } - } -} - -// extractWriteData extracts write data (single request or multiple requests) -// from the given wrapped requests set -func extractWriteData(wrappedRequests wrappedRPCRequests) any { - if len(wrappedRequests) == 1 { - return wrappedRequests[0].extractRPCRequest() - } - - return wrappedRequests.extractRPCRequests() -} diff --git a/tm2/pkg/bft/rpc/lib/client/ws_client_test.go b/tm2/pkg/bft/rpc/lib/client/ws_client_test.go deleted file mode 100644 index 9f0cccbde6b..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/ws_client_test.go +++ /dev/null @@ -1,264 +0,0 @@ -package rpcclient - -import ( - "context" - "encoding/json" - "net" - "net/http" - "net/http/httptest" - "sync" - "testing" - "time" - - "github.com/gorilla/websocket" - "github.com/stretchr/testify/require" - - types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/log" -) - -var wsCallTimeout = 5 * time.Second - -type myHandler struct { - closeConnAfterRead bool - mtx sync.RWMutex -} - -var upgrader = websocket.Upgrader{ - ReadBufferSize: 1024, - WriteBufferSize: 1024, -} - -func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - conn, err := upgrader.Upgrade(w, r, nil) - if err != nil { - panic(err) - } - defer conn.Close() //nolint: errcheck - for { - messageType, messageRaw, err := conn.ReadMessage() - if err != nil { - return - } - - // Read the message - var request types.RPCRequest - - if err := json.Unmarshal(messageRaw, &request); err != nil { - return - } - - h.mtx.RLock() - if h.closeConnAfterRead { - if err := conn.Close(); err != nil { - panic(err) - } - } - h.mtx.RUnlock() - - emptyRespBytes, err := json.Marshal(types.RPCResponse{ - JSONRPC: "2.0", - ID: request.ID, - Result: json.RawMessage(`""`), - }) - if err := conn.WriteMessage(messageType, emptyRespBytes); err != nil { - return - } - } -} - -func TestWSClientReconnectsAfterReadFailure(t *testing.T) { - t.Parallel() - - var wg sync.WaitGroup - - // start server - h := &myHandler{} - s := httptest.NewServer(h) - defer s.Close() - - c := startClient(t, s.Listener.Addr()) - defer c.Stop() - - wg.Add(1) - go callWgDoneOnResult(t, c, &wg) - - h.mtx.Lock() - h.closeConnAfterRead = true - h.mtx.Unlock() - - // results in WS read error, no send retry because write succeeded - call(t, "a", c) - - // expect to reconnect almost immediately - time.Sleep(10 * time.Millisecond) - h.mtx.Lock() - h.closeConnAfterRead = false - h.mtx.Unlock() - - // should succeed - call(t, "b", c) - - wg.Wait() -} - -func TestWSClientReconnectsAfterWriteFailure(t *testing.T) { - t.Parallel() - - var wg sync.WaitGroup - - // start server - h := &myHandler{} - s := httptest.NewServer(h) - - c := startClient(t, s.Listener.Addr()) - defer c.Stop() - - wg.Add(2) - go callWgDoneOnResult(t, c, &wg) - - // hacky way to abort the connection before write - if err := c.conn.Close(); err != nil { - t.Error(err) - } - - // results in WS write error, the client should resend on reconnect - call(t, "a", c) - - // expect to reconnect almost immediately - time.Sleep(10 * time.Millisecond) - - // should succeed - call(t, "b", c) - - wg.Wait() -} - -func TestWSClientReconnectFailure(t *testing.T) { - t.Parallel() - - // start server - h := &myHandler{} - s := httptest.NewServer(h) - - c := startClient(t, s.Listener.Addr()) - defer c.Stop() - - go func() { - for { - select { - case <-c.ResponsesCh: - case <-c.Quit(): - return - } - } - }() - - // hacky way to abort the connection before write - if err := c.conn.Close(); err != nil { - t.Error(err) - } - s.Close() - - // results in WS write error - // provide timeout to avoid blocking - ctx, cancel := context.WithTimeout(context.Background(), wsCallTimeout) - defer cancel() - - var result string - - if err := c.Call(ctx, "a", make(map[string]any), &result); err != nil { - t.Error(err) - } - - // expect to reconnect almost immediately - time.Sleep(10 * time.Millisecond) - - done := make(chan struct{}) - go func() { - // client should block on this - call(t, "b", c) - close(done) - }() - - // test that client blocks on the second send - select { - case <-done: - t.Fatal("client should block on calling 'b' during reconnect") - case <-time.After(5 * time.Second): - t.Log("All good") - } -} - -func TestNotBlockingOnStop(t *testing.T) { - t.Parallel() - - timeout := 2 * time.Second - s := httptest.NewServer(&myHandler{}) - c := startClient(t, s.Listener.Addr()) - - var result string - - c.Call(context.Background(), "a", make(map[string]any), &result) - // Let the readRoutine get around to blocking - time.Sleep(time.Second) - passCh := make(chan struct{}) - go func() { - // Unless we have a non-blocking write to ResponsesCh from readRoutine - // this blocks forever ont the waitgroup - c.Stop() - passCh <- struct{}{} - }() - select { - case <-passCh: - // Pass - case <-time.After(timeout): - t.Fatalf("WSClient did failed to stop within %v seconds - is one of the read/write routines blocking?", - timeout.Seconds()) - } -} - -func startClient(t *testing.T, addr net.Addr) *WSClient { - t.Helper() - - c := NewWSClient(addr.String(), "/websocket") - err := c.Start() - require.Nil(t, err) - c.SetLogger(log.TestingLogger()) - return c -} - -func call(t *testing.T, method string, c *WSClient) { - t.Helper() - - var result string - - err := c.Call(context.Background(), method, make(map[string]any), &result) - require.NoError(t, err) -} - -func callWgDoneOnResult(t *testing.T, c *WSClient, wg *sync.WaitGroup) { - t.Helper() - - for { - select { - case resps := <-c.ResponsesCh: - if len(resps) != 1 { - t.Errorf("invalid number of responses, %d", len(resps)) - - return - } - - resp := resps[0] - if resp.Error != nil { - t.Errorf("unexpected error: %v", resp.Error) - return - } - if resp.Result != nil { - wg.Done() - } - case <-c.Quit(): - return - } - } -} diff --git a/tm2/pkg/bft/rpc/lib/rpc_test.go b/tm2/pkg/bft/rpc/lib/rpc_test.go index 0203d10a24a..3dd914f4497 100644 --- a/tm2/pkg/bft/rpc/lib/rpc_test.go +++ b/tm2/pkg/bft/rpc/lib/rpc_test.go @@ -1,11 +1,6 @@ package rpc import ( - "bytes" - "context" - crand "crypto/rand" - "encoding/json" - "fmt" "net/http" "os" "os/exec" @@ -15,13 +10,11 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/gnolang/gno/tm2/pkg/colors" - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/random" - client "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" server "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/colors" + "github.com/gnolang/gno/tm2/pkg/log" ) // Client and Server should work over tcp or unix sockets @@ -33,8 +26,6 @@ const ( unixAddr = "unix://" + unixSocket websocketEndpoint = "/websocket/endpoint" - - testVal = "acbd" ) type ResultEcho struct { @@ -164,160 +155,7 @@ func echoViaHTTP(cl client.HTTPClient, val string) (string, error) { return result.Value, nil } -func echoIntViaHTTP(cl client.HTTPClient, val int) (int, error) { - params := map[string]interface{}{ - "arg": val, - } - result := new(ResultEchoInt) - if err := cl.Call("echo_int", params, result); err != nil { - return 0, err - } - return result.Value, nil -} - -func echoBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { - params := map[string]interface{}{ - "arg": bytes, - } - result := new(ResultEchoBytes) - if err := cl.Call("echo_bytes", params, result); err != nil { - return []byte{}, err - } - return result.Value, nil -} - -func echoDataBytesViaHTTP(cl client.HTTPClient, bytes []byte) ([]byte, error) { - params := map[string]interface{}{ - "arg": bytes, - } - result := new(ResultEchoDataBytes) - if err := cl.Call("echo_data_bytes", params, result); err != nil { - return []byte{}, err - } - return result.Value, nil -} - -func testWithHTTPClient(t *testing.T, cl client.HTTPClient) { - t.Helper() - - val := testVal - got, err := echoViaHTTP(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) - - val2 := randBytes(t) - got2, err := echoBytesViaHTTP(cl, val2) - require.Nil(t, err) - assert.Equal(t, got2, val2) - - val3 := randBytes(t) - got3, err := echoDataBytesViaHTTP(cl, val3) - require.Nil(t, err) - assert.Equal(t, got3, val3) - - val4 := random.RandIntn(10000) - got4, err := echoIntViaHTTP(cl, val4) - require.Nil(t, err) - assert.Equal(t, got4, val4) -} - -func echoViaWS(cl *client.WSClient, val string) (string, error) { - params := map[string]interface{}{ - "arg": val, - } - err := cl.Call(context.Background(), "echo", params, nil) - if err != nil { - return "", err - } - - msgs := <-cl.ResponsesCh - if len(msgs) != 1 { - return "", fmt.Errorf("invalid number of responses, %d", len(msgs)) - } - - msg := msgs[0] - - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - if err != nil { - return "", nil - } - return result.Value, nil -} - -func echoBytesViaWS(cl *client.WSClient, bytes []byte) ([]byte, error) { - params := map[string]interface{}{ - "arg": bytes, - } - err := cl.Call(context.Background(), "echo_bytes", params, nil) - if err != nil { - return []byte{}, err - } - - msgs := <-cl.ResponsesCh - if len(msgs) != 1 { - return nil, fmt.Errorf("invalid number of responses, %d", len(msgs)) - } - - msg := msgs[0] - if msg.Error != nil { - return []byte{}, msg.Error - } - result := new(ResultEchoBytes) - err = json.Unmarshal(msg.Result, result) - if err != nil { - return []byte{}, nil - } - return result.Value, nil -} - -func testWithWSClient(t *testing.T, cl *client.WSClient) { - t.Helper() - - val := testVal - got, err := echoViaWS(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) - - val2 := randBytes(t) - got2, err := echoBytesViaWS(cl, val2) - require.Nil(t, err) - assert.Equal(t, got2, val2) -} - // ------------- - -func TestServersAndClientsBasic(t *testing.T) { - t.Parallel() - - serverAddrs := [...]string{tcpAddr, unixAddr} - for _, addr := range serverAddrs { - cl1 := client.NewURIClient(addr) - fmt.Printf("=== testing server on %s using URI client", addr) - testWithHTTPClient(t, cl1) - - cl2 := client.NewJSONRPCClient(addr) - fmt.Printf("=== testing server on %s using JSONRPC client", addr) - testWithHTTPClient(t, cl2) - - cl3 := client.NewWSClient(addr, websocketEndpoint) - cl3.SetLogger(log.TestingLogger()) - err := cl3.Start() - require.Nil(t, err) - fmt.Printf("=== testing server on %s using WS client", addr) - testWithWSClient(t, cl3) - cl3.Stop() - } - - cl1 := client.NewURIClient(tcpServerUnavailableAddr) - err := cl1.Call("error", nil, nil) - require.EqualError(t, err, "server at 'http://0.0.0.0:47769' returned 418 I'm a teapot") - - cl2 := client.NewJSONRPCClient(tcpServerUnavailableAddr) - err = cl2.Call("error", nil, nil) - require.EqualError(t, err, "server at 'http://0.0.0.0:47769' returned 418 I'm a teapot") -} - func TestHexStringArg(t *testing.T) { t.Parallel() @@ -339,85 +177,3 @@ func TestQuotedStringArg(t *testing.T) { require.Nil(t, err) assert.Equal(t, got, val) } - -func TestWSNewWSRPCFunc(t *testing.T) { - t.Parallel() - - cl := client.NewWSClient(tcpAddr, websocketEndpoint) - cl.SetLogger(log.TestingLogger()) - err := cl.Start() - require.Nil(t, err) - defer cl.Stop() - - val := testVal - params := map[string]interface{}{ - "arg": val, - } - err = cl.Call(context.Background(), "echo_ws", params, nil) - require.Nil(t, err) - - msgs := <-cl.ResponsesCh - - for _, msg := range msgs { - if msg.Error != nil { - t.Fatal(err) - } - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - require.Nil(t, err) - got := result.Value - assert.Equal(t, got, val) - } -} - -func TestWSHandlesArrayParams(t *testing.T) { - t.Parallel() - - cl := client.NewWSClient(tcpAddr, websocketEndpoint) - cl.SetLogger(log.TestingLogger()) - err := cl.Start() - require.Nil(t, err) - defer cl.Stop() - - val := testVal - params := []interface{}{val} - err = cl.CallWithArrayParams(context.Background(), "echo_ws", params, nil) - require.Nil(t, err) - - msgs := <-cl.ResponsesCh - - for _, msg := range msgs { - if msg.Error != nil { - t.Fatalf("%+v", err) - } - result := new(ResultEcho) - err = json.Unmarshal(msg.Result, result) - require.Nil(t, err) - got := result.Value - assert.Equal(t, got, val) - } -} - -// TestWSClientPingPong checks that a client & server exchange pings -// & pongs so connection stays alive. -func TestWSClientPingPong(t *testing.T) { - t.Parallel() - - cl := client.NewWSClient(tcpAddr, websocketEndpoint) - cl.SetLogger(log.TestingLogger()) - err := cl.Start() - require.Nil(t, err) - defer cl.Stop() - - time.Sleep(6 * time.Second) -} - -func randBytes(t *testing.T) []byte { - t.Helper() - - n := random.RandIntn(10) + 2 - buf := make([]byte, n) - _, err := crand.Read(buf) - require.Nil(t, err) - return bytes.Replace(buf, []byte("="), []byte{100}, -1) -} diff --git a/tm2/pkg/bft/rpc/lib/types/types.go b/tm2/pkg/bft/rpc/lib/types/types.go index 43afa3a9811..e92ee7d49f5 100644 --- a/tm2/pkg/bft/rpc/lib/types/types.go +++ b/tm2/pkg/bft/rpc/lib/types/types.go @@ -6,7 +6,6 @@ import ( "fmt" "net/http" "reflect" - "strings" "github.com/gnolang/gno/tm2/pkg/amino" "github.com/gnolang/gno/tm2/pkg/errors" @@ -16,6 +15,7 @@ import ( // TODO: refactor when Go 2.0 arrives https://github.com/golang/go/issues/19412 type JSONRPCID interface { IsJSONRPCID() + String() string } // JSONRPCStringID a wrapper for JSON-RPC string IDs @@ -23,11 +23,19 @@ type JSONRPCStringID string func (JSONRPCStringID) IsJSONRPCID() {} +func (id JSONRPCStringID) String() string { + return string(id) +} + // JSONRPCIntID a wrapper for JSON-RPC integer IDs type JSONRPCIntID int func (JSONRPCIntID) IsJSONRPCID() {} +func (id JSONRPCIntID) String() string { + return fmt.Sprintf("%d", id) +} + func idFromInterface(idInterface interface{}) (JSONRPCID, error) { switch id := idInterface.(type) { case string: @@ -110,23 +118,6 @@ func MapToRequest(id JSONRPCID, method string, params map[string]interface{}) (R return request, nil } -func ArrayToRequest(id JSONRPCID, method string, params []interface{}) (RPCRequest, error) { - params_ := make([]json.RawMessage, len(params)) - for i, value := range params { - valueJSON, err := amino.MarshalJSON(value) - if err != nil { - return RPCRequest{}, err - } - params_[i] = valueJSON - } - payload, err := json.Marshal(params_) // NOTE: Amino doesn't handle maps yet. - if err != nil { - return RPCRequest{}, err - } - request := NewRPCRequest(id, method, payload) - return request, nil -} - // ---------------------------------------- // RESPONSE @@ -191,7 +182,7 @@ func NewRPCSuccessResponse(id JSONRPCID, res interface{}) RPCResponse { if err != nil { return RPCInternalError(id, errors.Wrap(err, "Error marshalling response")) } - rawMsg = json.RawMessage(js) + rawMsg = js } return RPCResponse{JSONRPC: "2.0", ID: id, Result: rawMsg} @@ -301,17 +292,3 @@ func (ctx *Context) Context() context.Context { } return context.Background() } - -// ---------------------------------------- -// SOCKETS - -// Determine if its a unix or tcp socket. -// If tcp, must specify the port; `0.0.0.0` will return incorrectly as "unix" since there's no port -// TODO: deprecate -func SocketType(listenAddr string) string { - socketType := "unix" - if len(strings.Split(listenAddr, ":")) >= 2 { - socketType = "tcp" - } - return socketType -} From 8c8d967b88bb406de8df497aaae6540a94eef334 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 9 Apr 2024 13:01:49 +0200 Subject: [PATCH 05/26] Add initial unit tests for ws client request parsing --- tm2/pkg/bft/rpc/lib/client/ws/client.go | 29 +-- tm2/pkg/bft/rpc/lib/client/ws/client_test.go | 171 ++++++++++++++++++ tm2/pkg/bft/rpc/lib/client/ws/options.go | 2 +- tm2/pkg/bft/rpc/lib/client/ws/options_test.go | 1 + 4 files changed, 189 insertions(+), 14 deletions(-) create mode 100644 tm2/pkg/bft/rpc/lib/client/ws/options_test.go diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client.go b/tm2/pkg/bft/rpc/lib/client/ws/client.go index 2d3cfc2fa9d..3f1630f4317 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client.go @@ -5,7 +5,6 @@ import ( "encoding/json" "fmt" "hash/fnv" - "log/slog" "sync" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" @@ -24,7 +23,7 @@ type Client struct { conn *websocket.Conn - logger *slog.Logger + // logger *slog.Logger rpcURL string // the remote RPC URL of the node backlog chan any // Either a single RPC request, or a batch of RPC requests @@ -45,6 +44,8 @@ func NewClient(rpcURL string, opts ...Option) (*Client, error) { rpcURL: rpcURL, conn: conn, requestMap: make(map[string]responseCh), + backlog: make(chan any, 1), + // logger: log.NewNopLogger(), } ctx, cancelFn := context.WithCancel(context.Background()) @@ -140,18 +141,18 @@ func (c *Client) runWriteRoutine(ctx context.Context) { for { select { case <-ctx.Done(): - c.logger.Debug("write context finished") + // c.logger.Debug("write context finished") return case item := <-c.backlog: // Write the JSON request to the server if err := c.conn.WriteJSON(item); err != nil { - c.logger.Error("unable to send request", "err", err) + // c.logger.Error("unable to send request", "err", err) continue } - c.logger.Debug("successfully sent request", "request", item) + // c.logger.Debug("successfully sent request", "request", item) } } } @@ -161,15 +162,15 @@ func (c *Client) runReadRoutine(ctx context.Context) { for { select { case <-ctx.Done(): - c.logger.Debug("read context finished") + // c.logger.Debug("read context finished") return default: // Read the message from the active connection - _, data, err := c.conn.ReadMessage() // TODO check message type + _, data, err := c.conn.ReadMessage() if err != nil { if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { - c.logger.Error("failed to read response", "err", err) + // c.logger.Error("failed to read response", "err", err) return } @@ -188,7 +189,7 @@ func (c *Client) runReadRoutine(ctx context.Context) { var response types.RPCResponse if err := json.Unmarshal(data, &response); err != nil { - c.logger.Error("failed to parse response", "err", err, "data", string(data)) + // c.logger.Error("failed to parse response", "err", err, "data", string(data)) continue } @@ -213,7 +214,7 @@ func (c *Client) runReadRoutine(ctx context.Context) { ch := c.requestMap[responseHash] if ch == nil { c.requestMapMux.Unlock() - c.logger.Error("response listener not set", "hash", responseHash, "responses", responses) + // c.logger.Error("response listener not set", "hash", responseHash, "responses", responses) continue } @@ -222,19 +223,21 @@ func (c *Client) runReadRoutine(ctx context.Context) { delete(c.requestMap, responseHash) c.requestMapMux.Unlock() - c.logger.Debug("received response", "hash", responseHash) + // c.logger.Debug("received response", "hash", responseHash) // Alert the listener of the response select { case ch <- responses: default: - c.logger.Warn("response listener timed out", "hash", responseHash) + // c.logger.Warn("response listener timed out", "hash", responseHash) } } } } // Close closes the WS client -func (c *Client) Close() { +func (c *Client) Close() error { c.cancelFn() + + return c.conn.Close() } diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client_test.go b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go index 98592950df4..ea9916c4e17 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client_test.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go @@ -1 +1,172 @@ package ws + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// createTestServer creates a test HTTP server +func createTestServer( + t *testing.T, + handler http.Handler, +) *httptest.Server { + t.Helper() + + s := httptest.NewServer(handler) + t.Cleanup(s.Close) + + return s +} + +func TestClient_SendRequest(t *testing.T) { + t.Parallel() + + t.Run("request timed out", func(t *testing.T) { + t.Parallel() + + var ( + upgrader = websocket.Upgrader{} + + request = types.RPCRequest{ + JSONRPC: "2.0", + ID: types.JSONRPCStringID("id"), + } + ) + + ctx, cancelFn := context.WithCancel(context.Background()) + defer cancelFn() + + // Create the server + handler := func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + require.NoError(t, err) + + defer c.Close() + + for { + _, message, err := c.ReadMessage() + if websocket.IsUnexpectedCloseError(err) { + return + } + + require.NoError(t, err) + + // Parse the message + var req types.RPCRequest + require.NoError(t, json.Unmarshal(message, &req)) + require.Equal(t, request.ID.String(), req.ID.String()) + + // Simulate context cancellation mid-request parsing + cancelFn() + } + } + + s := createTestServer(t, http.HandlerFunc(handler)) + url := "ws" + strings.TrimPrefix(s.URL, "http") + + // Create the client + c, err := NewClient(url) + require.NoError(t, err) + + defer func() { + assert.NoError(t, c.Close()) + }() + + // Try to send the request, but wait for + // the context to be cancelled + response, err := c.SendRequest(ctx, request) + require.Nil(t, response) + + assert.ErrorIs(t, err, errTimedOut) + }) + + t.Run("valid request sent", func(t *testing.T) { + t.Parallel() + + var ( + upgrader = websocket.Upgrader{} + + request = types.RPCRequest{ + JSONRPC: "2.0", + ID: types.JSONRPCStringID("id"), + } + + response = types.RPCResponse{ + JSONRPC: "2.0", + ID: request.ID, + } + ) + + // Create the server + handler := func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + require.NoError(t, err) + + defer c.Close() + + for { + mt, message, err := c.ReadMessage() + if websocket.IsUnexpectedCloseError(err) { + return + } + + require.NoError(t, err) + + // Parse the message + var req types.RPCRequest + require.NoError(t, json.Unmarshal(message, &req)) + require.Equal(t, request.ID.String(), req.ID.String()) + + marshalledResponse, err := json.Marshal(response) + require.NoError(t, err) + + require.NoError(t, c.WriteMessage(mt, marshalledResponse)) + } + } + + s := createTestServer(t, http.HandlerFunc(handler)) + url := "ws" + strings.TrimPrefix(s.URL, "http") + + // Create the client + c, err := NewClient(url) + require.NoError(t, err) + + defer func() { + assert.NoError(t, c.Close()) + }() + + // Try to send the valid request + ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFn() + + resp, err := c.SendRequest(ctx, request) + require.NoError(t, err) + + assert.Equal(t, response.ID, resp.ID) + assert.Equal(t, response.JSONRPC, resp.JSONRPC) + assert.Equal(t, response.Result, resp.Result) + assert.Equal(t, response.Error, resp.Error) + }) +} + +func TestClient_SendBatch(t *testing.T) { + t.Parallel() + + t.Run("batch timed out", func(t *testing.T) { + t.Parallel() // TODO implement + }) + + t.Run("valid batch sent", func(t *testing.T) { + t.Parallel() // TODO implement + }) +} diff --git a/tm2/pkg/bft/rpc/lib/client/ws/options.go b/tm2/pkg/bft/rpc/lib/client/ws/options.go index c98e8923b22..89429ec35bf 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/options.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/options.go @@ -9,6 +9,6 @@ type Option func(*Client) // WithLogger sets the WS client logger func WithLogger(logger *slog.Logger) Option { return func(c *Client) { - c.logger = logger + // c.logger = logger } } diff --git a/tm2/pkg/bft/rpc/lib/client/ws/options_test.go b/tm2/pkg/bft/rpc/lib/client/ws/options_test.go new file mode 100644 index 00000000000..98592950df4 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/ws/options_test.go @@ -0,0 +1 @@ +package ws From 615b9632e1992bc2cafb9b4bf50c21ad1a999e9d Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 9 Apr 2024 14:33:58 +0200 Subject: [PATCH 06/26] Add additional unit tests for ws client request parsing --- tm2/pkg/bft/rpc/lib/client/ws/client_test.go | 134 +++++++++++++++++- tm2/pkg/bft/rpc/lib/client/ws/options_test.go | 15 ++ 2 files changed, 147 insertions(+), 2 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client_test.go b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go index ea9916c4e17..e998c0891c5 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client_test.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go @@ -163,10 +163,140 @@ func TestClient_SendBatch(t *testing.T) { t.Parallel() t.Run("batch timed out", func(t *testing.T) { - t.Parallel() // TODO implement + t.Parallel() + + var ( + upgrader = websocket.Upgrader{} + + request = types.RPCRequest{ + JSONRPC: "2.0", + ID: types.JSONRPCStringID("id"), + } + + batch = types.RPCRequests{request} + ) + + ctx, cancelFn := context.WithCancel(context.Background()) + defer cancelFn() + + // Create the server + handler := func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + require.NoError(t, err) + + defer c.Close() + + for { + _, message, err := c.ReadMessage() + if websocket.IsUnexpectedCloseError(err) { + return + } + + require.NoError(t, err) + + // Parse the message + var req types.RPCRequests + require.NoError(t, json.Unmarshal(message, &req)) + + require.Len(t, req, 1) + require.Equal(t, request.ID.String(), req[0].ID.String()) + + // Simulate context cancellation mid-request parsing + cancelFn() + } + } + + s := createTestServer(t, http.HandlerFunc(handler)) + url := "ws" + strings.TrimPrefix(s.URL, "http") + + // Create the client + c, err := NewClient(url) + require.NoError(t, err) + + defer func() { + assert.NoError(t, c.Close()) + }() + + // Try to send the request, but wait for + // the context to be cancelled + response, err := c.SendBatch(ctx, batch) + require.Nil(t, response) + + assert.ErrorIs(t, err, errTimedOut) }) t.Run("valid batch sent", func(t *testing.T) { - t.Parallel() // TODO implement + t.Parallel() + + var ( + upgrader = websocket.Upgrader{} + + request = types.RPCRequest{ + JSONRPC: "2.0", + ID: types.JSONRPCStringID("id"), + } + + response = types.RPCResponse{ + JSONRPC: "2.0", + ID: request.ID, + } + + batch = types.RPCRequests{request} + batchResponse = types.RPCResponses{response} + ) + + // Create the server + handler := func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + require.NoError(t, err) + + defer c.Close() + + for { + mt, message, err := c.ReadMessage() + if websocket.IsUnexpectedCloseError(err) { + return + } + + require.NoError(t, err) + + // Parse the message + var req types.RPCRequests + require.NoError(t, json.Unmarshal(message, &req)) + + require.Len(t, req, 1) + require.Equal(t, request.ID.String(), req[0].ID.String()) + + marshalledResponse, err := json.Marshal(batchResponse) + require.NoError(t, err) + + require.NoError(t, c.WriteMessage(mt, marshalledResponse)) + } + } + + s := createTestServer(t, http.HandlerFunc(handler)) + url := "ws" + strings.TrimPrefix(s.URL, "http") + + // Create the client + c, err := NewClient(url) + require.NoError(t, err) + + defer func() { + assert.NoError(t, c.Close()) + }() + + // Try to send the valid request + ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFn() + + resp, err := c.SendBatch(ctx, batch) + require.NoError(t, err) + + require.Len(t, resp, 1) + + assert.Equal(t, response.ID, resp[0].ID) + assert.Equal(t, response.JSONRPC, resp[0].JSONRPC) + assert.Equal(t, response.Result, resp[0].Result) + assert.Equal(t, response.Error, resp[0].Error) }) } diff --git a/tm2/pkg/bft/rpc/lib/client/ws/options_test.go b/tm2/pkg/bft/rpc/lib/client/ws/options_test.go index 98592950df4..68dce10f5c9 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/options_test.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/options_test.go @@ -1 +1,16 @@ package ws + +// TODO uncomment after merging master +// func TestClient_WithLogger(t *testing.T) { +// t.Parallel() +// +// s := createTestServer(t, nil) +// url := "ws" + strings.TrimPrefix(s.URL, "http") +// +// // Create the client +// logger := ... +// c, err := NewClient(url, WithLogger(logger)) +// require.NoError(t, err) +// +// assert.Equal(t, logger, c.logger) +// } From 437eb50c66416783480e0223dc44414cf0ff5afe Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 9 Apr 2024 16:47:43 +0200 Subject: [PATCH 07/26] Add unit tests for the http client --- docs/how-to-guides/connecting-from-go.md | 4 +- gno.land/pkg/gnoclient/example_test.go | 6 +- gno.land/pkg/gnoclient/integration_test.go | 16 +- tm2/pkg/bft/rpc/client/http.go | 9 +- tm2/pkg/bft/rpc/client/rpc_test.go | 2 +- tm2/pkg/bft/rpc/lib/client/args_test.go | 41 -- tm2/pkg/bft/rpc/lib/client/batch.go | 17 +- tm2/pkg/bft/rpc/lib/client/http/client.go | 208 ++++++++++ .../bft/rpc/lib/client/http/client_test.go | 216 ++++++++++ tm2/pkg/bft/rpc/lib/client/http_client.go | 374 ------------------ .../bft/rpc/lib/client/http_client_test.go | 58 --- .../bft/rpc/lib/client/integration_test.go | 70 ---- tm2/pkg/bft/rpc/lib/client/uri_client.go | 11 +- tm2/pkg/bft/rpc/lib/client/ws/client.go | 27 +- tm2/pkg/bft/rpc/lib/client/ws/options.go | 2 +- tm2/pkg/bft/rpc/lib/client/ws/options_test.go | 50 ++- tm2/pkg/bft/rpc/lib/rpc_test.go | 3 +- tm2/pkg/bft/rpc/test/helpers.go | 2 +- 18 files changed, 511 insertions(+), 605 deletions(-) delete mode 100644 tm2/pkg/bft/rpc/lib/client/args_test.go create mode 100644 tm2/pkg/bft/rpc/lib/client/http/client.go create mode 100644 tm2/pkg/bft/rpc/lib/client/http/client_test.go delete mode 100644 tm2/pkg/bft/rpc/lib/client/http_client.go delete mode 100644 tm2/pkg/bft/rpc/lib/client/http_client_test.go delete mode 100644 tm2/pkg/bft/rpc/lib/client/integration_test.go diff --git a/docs/how-to-guides/connecting-from-go.md b/docs/how-to-guides/connecting-from-go.md index d1cdd324683..1fd47122371 100644 --- a/docs/how-to-guides/connecting-from-go.md +++ b/docs/how-to-guides/connecting-from-go.md @@ -109,7 +109,7 @@ A few things to note: You can initialize the RPC Client used to connect to the Gno.land network with the following line: ```go -rpc := rpcclient.NewHTTP("", "") +rpc := rpcclient.NewHTTP("") ``` A list of Gno.land network endpoints & chain IDs can be found in the [Gno RPC @@ -138,7 +138,7 @@ func main() { } // Initialize the RPC client - rpc := rpcclient.NewHTTP("", "") + rpc := rpcclient.NewHTTP("") // Initialize the gnoclient client := gnoclient.Client{ diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go index 08c3bf19066..a6a711bf0a2 100644 --- a/gno.land/pkg/gnoclient/example_test.go +++ b/gno.land/pkg/gnoclient/example_test.go @@ -16,7 +16,7 @@ func Example_withDisk() { } remote := "127.0.0.1:26657" - rpcClient := rpcclient.NewHTTP(remote, "/websocket") + rpcClient := rpcclient.NewHTTP(remote) client := gnoclient.Client{ Signer: signer, @@ -35,7 +35,7 @@ func Example_withInMemCrypto() { signer, _ := gnoclient.SignerFromBip39(mnemo, chainID, bip39Passphrase, account, index) remote := "127.0.0.1:26657" - rpcClient := rpcclient.NewHTTP(remote, "/websocket") + rpcClient := rpcclient.NewHTTP(remote) client := gnoclient.Client{ Signer: signer, @@ -47,7 +47,7 @@ func Example_withInMemCrypto() { // Example_readOnly demonstrates how to initialize a read-only gnoclient, which can only query. func Example_readOnly() { remote := "127.0.0.1:26657" - rpcClient := rpcclient.NewHTTP(remote, "/websocket") + rpcClient := rpcclient.NewHTTP(remote) client := gnoclient.Client{ RPCClient: rpcClient, diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index 3244b32af3f..a35ffb6fab3 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -25,7 +25,7 @@ func TestCallSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) // Setup Client client := Client{ @@ -68,7 +68,7 @@ func TestCallMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) // Setup Client client := Client{ @@ -119,7 +119,7 @@ func TestSendSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) // Setup Client client := Client{ @@ -167,7 +167,7 @@ func TestSendMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) // Setup Client client := Client{ @@ -223,7 +223,7 @@ func TestRunSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) client := Client{ Signer: signer, @@ -281,7 +281,7 @@ func TestRunMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) client := Client{ Signer: signer, @@ -361,7 +361,7 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) // Setup Client client := Client{ @@ -429,7 +429,7 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr, "/websocket") + rpcClient := rpcclient.NewHTTP(remoteAddr) // Setup Client client := Client{ diff --git a/tm2/pkg/bft/rpc/client/http.go b/tm2/pkg/bft/rpc/client/http.go index 714b83cc1b6..388d6fa1b69 100644 --- a/tm2/pkg/bft/rpc/client/http.go +++ b/tm2/pkg/bft/rpc/client/http.go @@ -5,6 +5,7 @@ import ( "net/http" rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" + http2 "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" ) /* @@ -21,7 +22,7 @@ the example for more details. */ type HTTP struct { remote string - rpc *rpcclient.JSONRPCClient + rpc *http2.Client *baseRPCClient } @@ -44,7 +45,7 @@ type Batch struct { // baseRPCClient implements the basic RPC method logic without the actual // underlying RPC call functionality, which is provided by `caller`. type baseRPCClient struct { - caller rpcclient.RPCCaller + caller http2.RPCCaller } var ( @@ -58,7 +59,7 @@ var ( // NewHTTP takes a remote endpoint in the form ://: // The function panics if the provided remote is invalid. func NewHTTP(remote string) *HTTP { - httpClient := rpcclient.DefaultHTTPClient(remote) + httpClient := http2.DefaultHTTPClient(remote) return NewHTTPWithClient(remote, httpClient) } @@ -68,7 +69,7 @@ func NewHTTPWithClient(remote string, client *http.Client) *HTTP { if client == nil { panic("nil http.Client provided") } - rc := rpcclient.NewJSONRPCClientWithHTTPClient(remote, client) + rc := http2.NewJSONRPCClientWithHTTPClient(remote, client) return &HTTP{ rpc: rc, diff --git a/tm2/pkg/bft/rpc/client/rpc_test.go b/tm2/pkg/bft/rpc/client/rpc_test.go index 3a0bef525ef..c1bc7779f36 100644 --- a/tm2/pkg/bft/rpc/client/rpc_test.go +++ b/tm2/pkg/bft/rpc/client/rpc_test.go @@ -7,12 +7,12 @@ import ( "sync" "testing" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" rpctest "github.com/gnolang/gno/tm2/pkg/bft/rpc/test" "github.com/gnolang/gno/tm2/pkg/bft/types" ) diff --git a/tm2/pkg/bft/rpc/lib/client/args_test.go b/tm2/pkg/bft/rpc/lib/client/args_test.go deleted file mode 100644 index 8119cb73b36..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/args_test.go +++ /dev/null @@ -1,41 +0,0 @@ -package rpcclient - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -type Tx []byte - -type Foo struct { - Bar int - Baz string -} - -func TestArgToJSON(t *testing.T) { - t.Parallel() - - assert := assert.New(t) - require := require.New(t) - - cases := []struct { - input any - expected string - }{ - {[]byte("1234"), "0x31323334"}, - {Tx("654"), "0x363534"}, - {Foo{7, "hello"}, `{"Bar":"7","Baz":"hello"}`}, - } - - for i, tc := range cases { - args := map[string]any{"data": tc.input} - err := argsToJSON(args) - require.Nil(err, "%d: %+v", i, err) - require.Equal(1, len(args), "%d", i) - data, ok := args["data"].(string) - require.True(ok, "%d: %#v", i, args["data"]) - assert.Equal(tc.expected, data, "%d", i) - } -} diff --git a/tm2/pkg/bft/rpc/lib/client/batch.go b/tm2/pkg/bft/rpc/lib/client/batch.go index 662413ee874..ed2416a6662 100644 --- a/tm2/pkg/bft/rpc/lib/client/batch.go +++ b/tm2/pkg/bft/rpc/lib/client/batch.go @@ -5,16 +5,17 @@ import ( "fmt" "sync" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" "github.com/gnolang/gno/tm2/pkg/random" ) type BatchClient interface { - SendBatch(ctx context.Context, requests WrappedRPCRequests) (types.RPCResponses, error) + SendBatch(ctx context.Context, requests http.WrappedRPCRequests) (types.RPCResponses, error) GetIDPrefix() types.JSONRPCID } -var _ RPCCaller = (*RPCRequestBatch)(nil) +var _ http.RPCCaller = (*RPCRequestBatch)(nil) // RPCRequestBatch allows us to buffer multiple request/response structures // into a single batch request. Note that this batch acts like a FIFO queue, and @@ -23,14 +24,14 @@ type RPCRequestBatch struct { sync.Mutex client BatchClient - requests WrappedRPCRequests + requests http.WrappedRPCRequests } // NewRPCRequestBatch creates a new func NewRPCRequestBatch(client BatchClient) *RPCRequestBatch { return &RPCRequestBatch{ client: client, - requests: make(WrappedRPCRequests, 0), + requests: make(http.WrappedRPCRequests, 0), } } @@ -52,7 +53,7 @@ func (b *RPCRequestBatch) Clear() int { func (b *RPCRequestBatch) clear() int { count := len(b.requests) - b.requests = make(WrappedRPCRequests, 0) + b.requests = make(http.WrappedRPCRequests, 0) return count } @@ -80,7 +81,7 @@ func (b *RPCRequestBatch) Send(ctx context.Context) ([]any, error) { return nil, err } - if err := unmarshalResponsesIntoResults(requests, responses, results); err != nil { + if err := http.unmarshalResponsesIntoResults(requests, responses, results); err != nil { return nil, err } @@ -101,7 +102,7 @@ func (b *RPCRequestBatch) Call(method string, params map[string]any, result any) } b.enqueue( - &WrappedRPCRequest{ + &http.WrappedRPCRequest{ request: request, result: result, }, @@ -110,7 +111,7 @@ func (b *RPCRequestBatch) Call(method string, params map[string]any, result any) return nil } -func (b *RPCRequestBatch) enqueue(req *WrappedRPCRequest) { +func (b *RPCRequestBatch) enqueue(req *http.WrappedRPCRequest) { b.Lock() defer b.Unlock() diff --git a/tm2/pkg/bft/rpc/lib/client/http/client.go b/tm2/pkg/bft/rpc/lib/client/http/client.go new file mode 100644 index 00000000000..5a05d2e966f --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/http/client.go @@ -0,0 +1,208 @@ +package http + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "strings" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" +) + +const ( + protoHTTP = "http" + protoHTTPS = "https" + protoWSS = "wss" + protoWS = "ws" + protoTCP = "tcp" +) + +// Client is an HTTP client implementation +type Client struct { + rpcURL string // the remote RPC URL of the node + + client *http.Client +} + +// NewClient initializes and creates a new HTTP RPC client +func NewClient(rpcURL string) (*Client, error) { + // Parse the RPC URL + address, err := toClientAddress(rpcURL) + if err != nil { + return nil, fmt.Errorf("invalid RPC URL, %w", err) + } + + c := &Client{ + rpcURL: address, + client: DefaultHTTPClient(rpcURL), + } + + return c, nil +} + +// SendRequest sends a single RPC request to the server +func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) { + return sendRequestCommon[types.RPCRequest, types.RPCResponse](ctx, c.client, c.rpcURL, request) +} + +// SendBatch sends a single RPC batch request to the server +func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (*types.RPCResponses, error) { + return sendRequestCommon[types.RPCRequests, types.RPCResponses](ctx, c.client, c.rpcURL, requests) +} + +type ( + requestType interface { + types.RPCRequest | types.RPCRequests + } + + responseType interface { + types.RPCResponse | types.RPCResponses + } +) + +// sendRequestCommon executes the common request sending +func sendRequestCommon[T requestType, R responseType]( + ctx context.Context, + client *http.Client, + rpcURL string, + request T, +) (*R, error) { + // Marshal the request + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, fmt.Errorf("unable to JSON-marshal the request, %w", err) + } + + // Craft the request + req, err := http.NewRequest( + http.MethodPost, + rpcURL, + bytes.NewBuffer(requestBytes), + ) + if err != nil { + return nil, fmt.Errorf("unable to create request, %w", err) + } + + // Set the header content type + req.Header.Set("Content-Type", "application/json") + + // Execute the request + httpResponse, err := client.Do(req.WithContext(ctx)) + if err != nil { + return nil, fmt.Errorf("unable to send request, %w", err) + } + defer httpResponse.Body.Close() //nolint: errcheck + + // Parse the response code + if !isOKStatus(httpResponse.StatusCode) { + return nil, fmt.Errorf("invalid status code received, %d", httpResponse.StatusCode) + } + + // Parse the response body + responseBytes, err := io.ReadAll(httpResponse.Body) + if err != nil { + return nil, fmt.Errorf("unable to read response body, %w", err) + } + + var response R + + if err := json.Unmarshal(responseBytes, &response); err != nil { + return nil, fmt.Errorf("unable to unmarshal response body, %w", err) + } + + return &response, nil +} + +// isOKStatus returns a boolean indicating if the response +// status code is between 200 and 299 (inclusive) +func isOKStatus(code int) bool { return code >= 200 && code <= 299 } + +// protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp") +// trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "." +func toClientAddrAndParse(remoteAddr string) (string, string, error) { + protocol, address, err := parseRemoteAddr(remoteAddr) + if err != nil { + return "", "", err + } + + // protocol to use for http operations, to support both http and https + var clientProtocol string + // default to http for unknown protocols (ex. tcp) + switch protocol { + case protoHTTP, protoHTTPS, protoWS, protoWSS: + clientProtocol = protocol + default: + clientProtocol = protoHTTP + } + + // replace / with . for http requests (kvstore domain) + trimmedAddress := strings.Replace(address, "/", ".", -1) + + return clientProtocol, trimmedAddress, nil +} + +func toClientAddress(remoteAddr string) (string, error) { + clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr) + if err != nil { + return "", err + } + + return clientProtocol + "://" + trimmedAddress, nil +} + +// network - name of the network (for example, "tcp", "unix") +// s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") +// TODO: Deprecate support for IP:PORT or /path/to/socket +func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { + parts := strings.SplitN(remoteAddr, "://", 2) + var protocol, address string + switch { + case len(parts) == 1: + // default to tcp if nothing specified + protocol, address = protoTCP, remoteAddr + case len(parts) == 2: + protocol, address = parts[0], parts[1] + default: + return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) + } + + return protocol, address, nil +} + +func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) { + protocol, address, err := parseRemoteAddr(remoteAddr) + if err != nil { + return func(_ string, _ string) (net.Conn, error) { + return nil, err + } + } + + // net.Dial doesn't understand http/https, so change it to TCP + switch protocol { + case protoHTTP, protoHTTPS: + protocol = protoTCP + } + + return func(proto, addr string) (net.Conn, error) { + return net.Dial(protocol, address) + } +} + +// DefaultHTTPClient is used to create an http client with some default parameters. +// We overwrite the http.Client.Dial so we can do http over tcp or unix. +// remoteAddr should be fully featured (eg. with tcp:// or unix://) +func DefaultHTTPClient(remoteAddr string) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + // Set to true to prevent GZIP-bomb DoS attacks + DisableCompression: true, + DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { + return makeHTTPDialer(remoteAddr)(network, addr) + }, + }, + } +} diff --git a/tm2/pkg/bft/rpc/lib/client/http/client_test.go b/tm2/pkg/bft/rpc/lib/client/http/client_test.go new file mode 100644 index 00000000000..c24d024abf5 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/http/client_test.go @@ -0,0 +1,216 @@ +package http + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/jaekwon/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestClient_parseRemoteAddr(t *testing.T) { + t.Parallel() + + testTable := []struct { + remoteAddr string + network string + rest string + }{ + { + "127.0.0.1", + "tcp", + "127.0.0.1", + }, + { + "https://example.com", + "https", + "example.com", + }, + { + "wss://[::1]", + "wss", + "[::1]", + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.remoteAddr, func(t *testing.T) { + t.Parallel() + + n, r, err := parseRemoteAddr(testCase.remoteAddr) + require.NoError(t, err) + + assert.Equal(t, n, testCase.network) + assert.Equal(t, r, testCase.rest) + }) + } +} + +// Following tests check that we correctly translate http/https to tcp, +// and other protocols are left intact from parseRemoteAddr() + +func TestClient_makeHTTPDialer(t *testing.T) { + t.Parallel() + + t.Run("http", func(t *testing.T) { + t.Parallel() + + _, err := makeHTTPDialer("https://.")("hello", "world") + require.Error(t, err) + + assert.Contains(t, err.Error(), "dial tcp:", "should convert https to tcp") + assert.Contains(t, err.Error(), "address .:", "should have parsed the address (as incorrect)") + }) + + t.Run("udp", func(t *testing.T) { + t.Parallel() + + _, err := makeHTTPDialer("udp://.")("hello", "world") + require.Error(t, err) + + assert.Contains(t, err.Error(), "dial udp:", "udp protocol should remain the same") + assert.Contains(t, err.Error(), "address .:", "should have parsed the address (as incorrect)") + }) +} + +// createTestServer creates a test HTTP server +func createTestServer( + t *testing.T, + handler http.Handler, +) *httptest.Server { + t.Helper() + + s := httptest.NewServer(handler) + t.Cleanup(s.Close) + + return s +} + +func TestClient_SendRequest(t *testing.T) { + t.Parallel() + + var ( + request = types.RPCRequest{ + JSONRPC: "2.0", + ID: types.JSONRPCStringID("id"), + } + + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "application/json", r.Header.Get("content-type")) + + // Parse the message + var req types.RPCRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + require.Equal(t, request.ID.String(), req.ID.String()) + + // Send an empty response back + response := types.RPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + } + + // Marshal the response + marshalledResponse, err := json.Marshal(response) + require.NoError(t, err) + + _, err = w.Write(marshalledResponse) + require.NoError(t, err) + }) + + server = createTestServer(t, handler) + ) + + // Create the client + c, err := NewClient(server.URL) + require.NoError(t, err) + + ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFn() + + // Send the request + resp, err := c.SendRequest(ctx, request) + require.NoError(t, err) + + assert.Equal(t, request.ID, resp.ID) + assert.Equal(t, request.JSONRPC, resp.JSONRPC) + assert.Nil(t, resp.Result) + assert.Nil(t, resp.Error) +} + +func TestClient_SendBatchRequest(t *testing.T) { + t.Parallel() + + var ( + request = types.RPCRequest{ + JSONRPC: "2.0", + ID: types.JSONRPCStringID("id"), + } + + requests = types.RPCRequests{ + request, + request, + } + + handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "application/json", r.Header.Get("content-type")) + + // Parse the message + var reqs types.RPCRequests + require.NoError(t, json.NewDecoder(r.Body).Decode(&reqs)) + require.Len(t, reqs, len(requests)) + + for _, req := range reqs { + require.Equal(t, request.ID.String(), req.ID.String()) + } + + // Send an empty response batch back + response := types.RPCResponse{ + JSONRPC: "2.0", + ID: request.ID, + } + + responses := types.RPCResponses{ + response, + response, + } + + // Marshal the responses + marshalledResponses, err := json.Marshal(responses) + require.NoError(t, err) + + _, err = w.Write(marshalledResponses) + require.NoError(t, err) + }) + + server = createTestServer(t, handler) + ) + + // Create the client + c, err := NewClient(server.URL) + require.NoError(t, err) + + ctx, cancelFn := context.WithTimeout(context.Background(), time.Second*5) + defer cancelFn() + + // Send the request + resps, err := c.SendBatch(ctx, requests) + require.NoError(t, err) + + require.Len(t, *resps, len(requests)) + + for _, resp := range *resps { + assert.Equal(t, request.ID, resp.ID) + assert.Equal(t, request.JSONRPC, resp.JSONRPC) + assert.Nil(t, resp.Result) + assert.Nil(t, resp.Error) + } +} diff --git a/tm2/pkg/bft/rpc/lib/client/http_client.go b/tm2/pkg/bft/rpc/lib/client/http_client.go deleted file mode 100644 index 0f6f2dca9d2..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/http_client.go +++ /dev/null @@ -1,374 +0,0 @@ -package rpcclient - -import ( - "bytes" - "context" - "encoding/json" - "fmt" - "io" - "net" - "net/http" - "net/url" - "reflect" - "strings" - - "github.com/gnolang/gno/tm2/pkg/amino" - types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/errors" - "github.com/gnolang/gno/tm2/pkg/random" -) - -const ( - protoHTTP = "http" - protoHTTPS = "https" - protoWSS = "wss" - protoWS = "ws" - protoTCP = "tcp" -) - -// HTTPClient is a common interface for JSONRPCClient and URIClient. -type HTTPClient interface { - Call(method string, params map[string]any, result any) error -} - -// protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp") -// trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "." -func toClientAddrAndParse(remoteAddr string) (network string, trimmedS string, err error) { - protocol, address, err := parseRemoteAddr(remoteAddr) - if err != nil { - return "", "", err - } - - // protocol to use for http operations, to support both http and https - var clientProtocol string - // default to http for unknown protocols (ex. tcp) - switch protocol { - case protoHTTP, protoHTTPS, protoWS, protoWSS: - clientProtocol = protocol - default: - clientProtocol = protoHTTP - } - - // replace / with . for http requests (kvstore domain) - trimmedAddress := strings.Replace(address, "/", ".", -1) - return clientProtocol, trimmedAddress, nil -} - -func toClientAddress(remoteAddr string) (string, error) { - clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr) - if err != nil { - return "", err - } - return clientProtocol + "://" + trimmedAddress, nil -} - -// network - name of the network (for example, "tcp", "unix") -// s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") -// TODO: Deprecate support for IP:PORT or /path/to/socket -func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { - parts := strings.SplitN(remoteAddr, "://", 2) - var protocol, address string - switch { - case len(parts) == 1: - // default to tcp if nothing specified - protocol, address = protoTCP, remoteAddr - case len(parts) == 2: - protocol, address = parts[0], parts[1] - default: - return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) - } - - return protocol, address, nil -} - -func makeErrorDialer(err error) func(string, string) (net.Conn, error) { - return func(_ string, _ string) (net.Conn, error) { - return nil, err - } -} - -func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) { - protocol, address, err := parseRemoteAddr(remoteAddr) - if err != nil { - return makeErrorDialer(err) - } - - // net.Dial doesn't understand http/https, so change it to TCP - switch protocol { - case protoHTTP, protoHTTPS: - protocol = protoTCP - } - - return func(proto, addr string) (net.Conn, error) { - return net.Dial(protocol, address) - } -} - -// DefaultHTTPClient is used to create an http client with some default parameters. -// We overwrite the http.Client.Dial so we can do http over tcp or unix. -// remoteAddr should be fully featured (eg. with tcp:// or unix://) -func DefaultHTTPClient(remoteAddr string) *http.Client { - return &http.Client{ - Transport: &http.Transport{ - // Set to true to prevent GZIP-bomb DoS attacks - DisableCompression: true, - Dial: makeHTTPDialer(remoteAddr), - }, - } -} - -// ------------------------------------------------------------------------------------ - -// JSONRPCClient takes params as a slice -type JSONRPCClient struct { - address string - client *http.Client - idPrefix types.JSONRPCStringID -} - -// RPCCaller implementers can facilitate calling the JSON RPC endpoint. -type RPCCaller interface { - Call(method string, params map[string]any, result any) error -} - -// WrappedRPCRequest encapsulates a single buffered request, as well as its -// anticipated response structure -type WrappedRPCRequest struct { - request types.RPCRequest - result any // The result will be deserialized into this object (Amino) -} - -type WrappedRPCRequests []*WrappedRPCRequest - -func (w *WrappedRPCRequest) extractRPCRequest() types.RPCRequest { - return w.request -} - -func (w *WrappedRPCRequests) extractRPCRequests() types.RPCRequests { - requests := make([]types.RPCRequest, 0, len(*w)) - - for _, wrappedRequest := range *w { - requests = append(requests, wrappedRequest.request) - } - - return requests -} - -var ( - _ RPCCaller = (*JSONRPCClient)(nil) - _ BatchClient = (*JSONRPCClient)(nil) -) - -// NewJSONRPCClient returns a JSONRPCClient pointed at the given address. -func NewJSONRPCClient(remote string) *JSONRPCClient { - return NewJSONRPCClientWithHTTPClient(remote, DefaultHTTPClient(remote)) -} - -// NewJSONRPCClientWithHTTPClient returns a JSONRPCClient pointed at the given address using a custom http client -// The function panics if the provided client is nil or remote is invalid. -func NewJSONRPCClientWithHTTPClient(remote string, client *http.Client) *JSONRPCClient { - if client == nil { - panic("nil http.Client provided") - } - - clientAddress, err := toClientAddress(remote) - if err != nil { - panic(fmt.Sprintf("invalid remote %s: %s", remote, err)) - } - - return &JSONRPCClient{ - address: clientAddress, - client: client, - idPrefix: types.JSONRPCStringID("jsonrpc-client-" + random.RandStr(8)), - } -} - -// 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]any, result any) error { - id := generateRequestID(c.idPrefix) - - request, err := types.MapToRequest(id, method, params) - if err != nil { - return err - } - requestBytes, err := json.Marshal(request) - if err != nil { - return err - } - requestBuf := bytes.NewBuffer(requestBytes) - httpResponse, err := c.client.Post(c.address, "text/json", requestBuf) - if err != nil { - return err - } - defer httpResponse.Body.Close() //nolint: errcheck - - if !statusOK(httpResponse.StatusCode) { - return errors.New("server at '%s' returned %s", c.address, httpResponse.Status) - } - - responseBytes, err := io.ReadAll(httpResponse.Body) - if err != nil { - return err - } - - var response types.RPCResponse - - err = json.Unmarshal(responseBytes, &response) - if err != nil { - return errors.Wrap(err, "error unmarshalling rpc response") - } - - if response.Error != nil { - return errors.Wrap(response.Error, "response error") - } - - return unmarshalResponseIntoResult(&response, id, result) -} - -func (c *JSONRPCClient) SendBatch(_ context.Context, wrappedRequests WrappedRPCRequests) (types.RPCResponses, error) { - requests := make(types.RPCRequests, 0, len(wrappedRequests)) - for _, request := range wrappedRequests { - requests = append(requests, request.request) - } - - // serialize the array of requests into a single JSON object - requestBytes, err := json.Marshal(requests) - 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 - - if !statusOK(httpResponse.StatusCode) { - return nil, errors.New("server at '%s' returned %s", c.address, httpResponse.Status) - } - - responseBytes, err := io.ReadAll(httpResponse.Body) - - var responses types.RPCResponses - - if err = json.Unmarshal(responseBytes, &responses); err != nil { - return nil, errors.Wrap(err, "error unmarshalling rpc responses") - } - - return responses, nil -} - -func (c *JSONRPCClient) GetIDPrefix() types.JSONRPCID { - return c.idPrefix -} - -func unmarshalResponseIntoResult(response *types.RPCResponse, expectedID types.JSONRPCID, result any) error { - // Read response. If rpc/core/types is imported, the result will unmarshal - // into the correct type. - // From the JSON-RPC 2.0 spec: - // idPrefix: It MUST be the same as the value of the idPrefix member in the Request Object. - if err := validateResponseID(response, expectedID); err != nil { - return err - } - - // Unmarshal the RawMessage into the result. - if err := amino.UnmarshalJSON(response.Result, result); err != nil { - return errors.Wrap(err, "error unmarshalling rpc response result") - } - - return nil -} - -func unmarshalResponsesIntoResults(requests types.RPCRequests, responses types.RPCResponses, results []any) error { - // No response error checking here as there may be a mixture of successful - // and unsuccessful responses - if len(results) != len(responses) { - return fmt.Errorf("expected %d result objects into which to inject responses, but got %d", len(responses), len(results)) - } - - for i, response := range responses { - response := response - // From the JSON-RPC 2.0 spec: - // idPrefix: It MUST be the same as the value of the idPrefix member in the Request Object. - - // This validation is super sketchy. Why do this here? - // This validation passes iff the server returns batch responses - // in the same order as the batch request - if err := validateResponseID(&response, requests[i].ID); err != nil { - return errors.Wrap(err, "failed to validate response ID in response %d", i) - } - if err := amino.UnmarshalJSON(responses[i].Result, results[i]); err != nil { - return errors.Wrap(err, "error unmarshalling rpc response result") - } - } - - return nil -} - -func validateResponseID(res *types.RPCResponse, expectedID types.JSONRPCID) error { - _, isNumValue := expectedID.(types.JSONRPCIntID) - stringValue, isStringValue := expectedID.(types.JSONRPCStringID) - - if !isNumValue && !isStringValue { - return errors.New("invalid expected ID") - } - - // we only validate a response ID if the expected ID is non-empty - if isStringValue && len(stringValue) == 0 { - return nil - } - - if res.ID == nil { - return errors.New("missing ID in response") - } - - if expectedID != res.ID { - return fmt.Errorf("response ID (%s) does not match request ID (%s)", res.ID, expectedID) - } - - return nil -} - -func argsToURLValues(args map[string]any) (url.Values, error) { - values := make(url.Values) - if len(args) == 0 { - return values, nil - } - err := argsToJSON(args) - if err != nil { - return nil, err - } - for key, val := range args { - values.Set(key, val.(string)) - } - return values, nil -} - -func argsToJSON(args map[string]any) error { - for k, v := range args { - rt := reflect.TypeOf(v) - isByteSlice := rt.Kind() == reflect.Slice && rt.Elem().Kind() == reflect.Uint8 - if isByteSlice { - bytes := reflect.ValueOf(v).Bytes() - args[k] = fmt.Sprintf("0x%X", bytes) - continue - } - - data, err := amino.MarshalJSON(v) - if err != nil { - return err - } - args[k] = string(data) - } - return nil -} - -func statusOK(code int) bool { return code >= 200 && code <= 299 } - -// generateRequestID generates a unique request ID, using the prefix -// Assuming this is sufficiently random, there shouldn't be any problems. -// However, using uuid for any kind of ID generation is always preferred -func generateRequestID(prefix types.JSONRPCID) types.JSONRPCID { - return types.JSONRPCStringID(fmt.Sprintf("%s-%s", prefix, random.RandStr(8))) -} diff --git a/tm2/pkg/bft/rpc/lib/client/http_client_test.go b/tm2/pkg/bft/rpc/lib/client/http_client_test.go deleted file mode 100644 index 460f5b9947b..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/http_client_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package rpcclient - -import ( - "testing" - - "github.com/jaekwon/testify/assert" -) - -func Test_parseRemoteAddr(t *testing.T) { - t.Parallel() - - tt := []struct { - remoteAddr string - network, s, errContains string - }{ - {"127.0.0.1", "tcp", "127.0.0.1", ""}, - {"https://example.com", "https", "example.com", ""}, - {"wss://[::1]", "wss", "[::1]", ""}, - // no error cases - they cannot happen! - } - - for _, tc := range tt { - n, s, err := parseRemoteAddr(tc.remoteAddr) - if tc.errContains != "" { - _ = assert.NotNil(t, err) && assert.Contains(t, err.Error(), tc.errContains) - } - assert.NoError(t, err) - assert.Equal(t, n, tc.network) - assert.Equal(t, s, tc.s) - } -} - -// Following tests check that we correctly translate http/https to tcp, -// and other protocols are left intact from parseRemoteAddr() - -func Test_makeHTTPDialer(t *testing.T) { - t.Parallel() - - dl := makeHTTPDialer("https://.") - _, err := dl("hello", "world") - if assert.NotNil(t, err) { - e := err.Error() - assert.Contains(t, e, "dial tcp:", "should convert https to tcp") - assert.Contains(t, e, "address .:", "should have parsed the address (as incorrect)") - } -} - -func Test_makeHTTPDialer_noConvert(t *testing.T) { - t.Parallel() - - dl := makeHTTPDialer("udp://.") - _, err := dl("hello", "world") - if assert.NotNil(t, err) { - e := err.Error() - assert.Contains(t, e, "dial udp:", "udp protocol should remain the same") - assert.Contains(t, e, "address .:", "should have parsed the address (as incorrect)") - } -} diff --git a/tm2/pkg/bft/rpc/lib/client/integration_test.go b/tm2/pkg/bft/rpc/lib/client/integration_test.go deleted file mode 100644 index 5cb4049f8a6..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/integration_test.go +++ /dev/null @@ -1,70 +0,0 @@ -//go:build release - -// The code in here is comprehensive as an integration -// test and is long, hence is only run before releases. - -package rpcclient - -import ( - "bytes" - "errors" - "net" - "regexp" - "testing" - "time" - - "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/ws" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/tm2/pkg/log" -) - -func TestWSClientReconnectWithJitter(t *testing.T) { - t.Parallel() - - n := 8 - maxReconnectAttempts := 3 - // Max wait time is ceil(1+0.999) + ceil(2+0.999) + ceil(4+0.999) + ceil(...) = 2 + 3 + 5 = 10s + ... - maxSleepTime := time.Second * time.Duration(((1< Date: Tue, 9 Apr 2024 17:50:41 +0200 Subject: [PATCH 08/26] Clean up batch functionality --- tm2/pkg/bft/rpc/client/http.go | 6 +- tm2/pkg/bft/rpc/lib/client/batch.go | 119 ------------------ tm2/pkg/bft/rpc/lib/client/batch/batch.go | 78 ++++++++++++ .../bft/rpc/lib/client/batch/batch_test.go | 103 +++++++++++++++ tm2/pkg/bft/rpc/lib/client/batch/mock_test.go | 21 ++++ tm2/pkg/bft/rpc/lib/client/client.go | 31 +++++ tm2/pkg/bft/rpc/lib/client/http/client.go | 112 +---------------- .../bft/rpc/lib/client/http/client_test.go | 4 +- tm2/pkg/bft/rpc/lib/client/http/utils.go | 107 ++++++++++++++++ tm2/pkg/bft/rpc/lib/client/uri_client.go | 65 ---------- tm2/pkg/bft/rpc/lib/rpc_test.go | 6 +- 11 files changed, 354 insertions(+), 298 deletions(-) delete mode 100644 tm2/pkg/bft/rpc/lib/client/batch.go create mode 100644 tm2/pkg/bft/rpc/lib/client/batch/batch.go create mode 100644 tm2/pkg/bft/rpc/lib/client/batch/batch_test.go create mode 100644 tm2/pkg/bft/rpc/lib/client/batch/mock_test.go create mode 100644 tm2/pkg/bft/rpc/lib/client/client.go create mode 100644 tm2/pkg/bft/rpc/lib/client/http/utils.go delete mode 100644 tm2/pkg/bft/rpc/lib/client/uri_client.go diff --git a/tm2/pkg/bft/rpc/client/http.go b/tm2/pkg/bft/rpc/client/http.go index 388d6fa1b69..8efc260091d 100644 --- a/tm2/pkg/bft/rpc/client/http.go +++ b/tm2/pkg/bft/rpc/client/http.go @@ -4,7 +4,7 @@ import ( "context" "net/http" - rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/batch" http2 "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" ) @@ -38,7 +38,7 @@ type HTTP struct { // batch, but ordering of transactions in the batch cannot be guaranteed in such // an example. type Batch struct { - rpcBatch *rpcclient.RPCRequestBatch + rpcBatch *rpcclient.Batch *baseRPCClient } @@ -80,7 +80,7 @@ func NewHTTPWithClient(remote string, client *http.Client) *HTTP { // NewBatch creates a new rpcBatch client for this HTTP client. func (c *HTTP) NewBatch() *Batch { - batch := rpcclient.NewRPCRequestBatch(c.rpc) + batch := rpcclient.NewBatch(c.rpc) return &Batch{ rpcBatch: batch, baseRPCClient: &baseRPCClient{ diff --git a/tm2/pkg/bft/rpc/lib/client/batch.go b/tm2/pkg/bft/rpc/lib/client/batch.go deleted file mode 100644 index ed2416a6662..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/batch.go +++ /dev/null @@ -1,119 +0,0 @@ -package rpcclient - -import ( - "context" - "fmt" - "sync" - - "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" - types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/random" -) - -type BatchClient interface { - SendBatch(ctx context.Context, requests http.WrappedRPCRequests) (types.RPCResponses, error) - GetIDPrefix() types.JSONRPCID -} - -var _ http.RPCCaller = (*RPCRequestBatch)(nil) - -// RPCRequestBatch 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 RPCRequestBatch struct { - sync.Mutex - - client BatchClient - requests http.WrappedRPCRequests -} - -// NewRPCRequestBatch creates a new -func NewRPCRequestBatch(client BatchClient) *RPCRequestBatch { - return &RPCRequestBatch{ - client: client, - requests: make(http.WrappedRPCRequests, 0), - } -} - -// Count returns the number of enqueued requests waiting to be sent -func (b *RPCRequestBatch) Count() int { - b.Lock() - defer b.Unlock() - - return len(b.requests) -} - -// Clear empties out the request batch -func (b *RPCRequestBatch) Clear() int { - b.Lock() - defer b.Unlock() - - return b.clear() -} - -func (b *RPCRequestBatch) clear() int { - count := len(b.requests) - b.requests = make(http.WrappedRPCRequests, 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 *RPCRequestBatch) Send(ctx context.Context) ([]any, error) { - b.Lock() - defer func() { - b.clear() - b.Unlock() - }() - - requests := make(types.RPCRequests, 0, len(b.requests)) - results := make([]any, 0, len(b.requests)) - - for _, req := range b.requests { - requests = append(requests, req.request) - results = append(results, req.result) - } - - responses, err := b.client.SendBatch(ctx, b.requests) - if err != nil { - return nil, err - } - - if err := http.unmarshalResponsesIntoResults(requests, responses, results); err != nil { - return nil, err - } - - return results, nil -} - -// Call enqueues a request to call the given RPC method with the specified parameters -func (b *RPCRequestBatch) Call(method string, params map[string]any, result any) error { - // Assuming this is sufficiently random, there shouldn't be any problems. - // However, using uuid for any kind of ID generation is always preferred - id := types.JSONRPCStringID( - fmt.Sprintf("%s-%s", b.client.GetIDPrefix(), random.RandStr(8)), - ) - - request, err := types.MapToRequest(id, method, params) - if err != nil { - return err - } - - b.enqueue( - &http.WrappedRPCRequest{ - request: request, - result: result, - }, - ) - - return nil -} - -func (b *RPCRequestBatch) enqueue(req *http.WrappedRPCRequest) { - b.Lock() - defer b.Unlock() - - b.requests = append(b.requests, req) -} diff --git a/tm2/pkg/bft/rpc/lib/client/batch/batch.go b/tm2/pkg/bft/rpc/lib/client/batch/batch.go new file mode 100644 index 00000000000..2cd7ba82bab --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/batch/batch.go @@ -0,0 +1,78 @@ +package batch + +import ( + "context" + "sync" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" +) + +type Client interface { + SendBatch(context.Context, types.RPCRequests) (types.RPCResponses, error) +} + +// Batch 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 Batch struct { + sync.RWMutex + + client Client + requests types.RPCRequests +} + +// NewBatch creates a new batch object +func NewBatch(client Client) *Batch { + return &Batch{ + client: client, + requests: make(types.RPCRequests, 0), + } +} + +// Count returns the number of enqueued requests waiting to be sent +func (b *Batch) Count() int { + b.RLock() + defer b.RUnlock() + + return len(b.requests) +} + +// Clear empties out the request batch +func (b *Batch) Clear() int { + b.Lock() + defer b.Unlock() + + return b.clear() +} + +func (b *Batch) clear() int { + count := len(b.requests) + b.requests = make(types.RPCRequests, 0) + + return count +} + +// Send will attempt to send the current batch of enqueued requests, and then +// will clear out the requests once done +func (b *Batch) Send(ctx context.Context) (types.RPCResponses, error) { + b.Lock() + defer func() { + b.clear() + b.Unlock() + }() + + responses, err := b.client.SendBatch(ctx, b.requests) + if err != nil { + return nil, err + } + + return responses, nil +} + +// AddRequest adds a new request onto the batch +func (b *Batch) AddRequest(request types.RPCRequest) { + b.Lock() + defer b.Unlock() + + b.requests = append(b.requests, request) +} diff --git a/tm2/pkg/bft/rpc/lib/client/batch/batch_test.go b/tm2/pkg/bft/rpc/lib/client/batch/batch_test.go new file mode 100644 index 00000000000..2ef01bb6360 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/batch/batch_test.go @@ -0,0 +1,103 @@ +package batch + +import ( + "context" + "testing" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateRequests generates dummy RPC requests +func generateRequests(t *testing.T, count int) types.RPCRequests { + t.Helper() + + requests := make(types.RPCRequests, 0, count) + + for i := 0; i < count; i++ { + requests = append(requests, types.RPCRequest{ + JSONRPC: "2.0", + ID: types.JSONRPCIntID(i), + }) + } + + return requests +} + +func TestBatch_AddRequest(t *testing.T) { + t.Parallel() + + var ( + capturedSend types.RPCRequests + requests = generateRequests(t, 100) + + mockClient = &mockClient{ + sendBatchFn: func(_ context.Context, requests types.RPCRequests) (types.RPCResponses, error) { + capturedSend = requests + + responses := make(types.RPCResponses, len(requests)) + + for index, request := range requests { + responses[index] = types.RPCResponse{ + JSONRPC: "2.0", + ID: request.ID, + } + } + + return responses, nil + }, + } + ) + + // Create the batch + b := NewBatch(mockClient) + + // Add the requests + for _, request := range requests { + b.AddRequest(request) + } + + // Make sure the count is correct + require.Equal(t, len(requests), b.Count()) + + // Send the requests + responses, err := b.Send(context.Background()) + require.NoError(t, err) + + // Make sure the correct requests were sent + assert.Equal(t, requests, capturedSend) + + // Make sure the correct responses were returned + require.Len(t, responses, len(requests)) + + for index, response := range responses { + assert.Equal(t, requests[index].ID, response.ID) + assert.Equal(t, requests[index].JSONRPC, response.JSONRPC) + assert.Nil(t, response.Result) + assert.Nil(t, response.Error) + } + + // Make sure the batch has been cleared after sending + assert.Equal(t, b.Count(), 0) +} + +func TestBatch_Clear(t *testing.T) { + t.Parallel() + + requests := generateRequests(t, 100) + + // Create the batch + b := NewBatch(nil) + + // Add the requests + for _, request := range requests { + b.AddRequest(request) + } + + // Clear the batch + require.EqualValues(t, len(requests), b.Clear()) + + // Make sure the batch is cleared + require.Equal(t, b.Count(), 0) +} diff --git a/tm2/pkg/bft/rpc/lib/client/batch/mock_test.go b/tm2/pkg/bft/rpc/lib/client/batch/mock_test.go new file mode 100644 index 00000000000..5865631feab --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/batch/mock_test.go @@ -0,0 +1,21 @@ +package batch + +import ( + "context" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" +) + +type sendBatchDelegate func(context.Context, types.RPCRequests) (types.RPCResponses, error) + +type mockClient struct { + sendBatchFn sendBatchDelegate +} + +func (m *mockClient) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) { + if m.sendBatchFn != nil { + return m.sendBatchFn(ctx, requests) + } + + return nil, nil +} diff --git a/tm2/pkg/bft/rpc/lib/client/client.go b/tm2/pkg/bft/rpc/lib/client/client.go new file mode 100644 index 00000000000..d033d811c57 --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/client.go @@ -0,0 +1,31 @@ +package rpcclient + +import ( + "context" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" +) + +// Client is the JSON-RPC client abstraction +type Client interface { + // SendRequest sends a single RPC request to the JSON-RPC layer + SendRequest(context.Context, types.RPCRequest) (*types.RPCResponse, error) + + // SendBatch sends a batch of RPC requests to the JSON-RPC layer + SendBatch(context.Context, types.RPCRequests) (types.RPCResponses, error) +} + +// Batch is the JSON-RPC batch abstraction +type Batch interface { + // AddRequest adds a single request to the RPC batch + AddRequest(types.RPCRequest) + + // Send sends the batch to the RPC layer + Send(context.Context) (types.RPCResponses, error) + + // Clear clears out the batch + Clear() int + + // Count returns the number of enqueued requests + Count() int +} diff --git a/tm2/pkg/bft/rpc/lib/client/http/client.go b/tm2/pkg/bft/rpc/lib/client/http/client.go index 5a05d2e966f..2ed46117d71 100644 --- a/tm2/pkg/bft/rpc/lib/client/http/client.go +++ b/tm2/pkg/bft/rpc/lib/client/http/client.go @@ -6,21 +6,11 @@ import ( "encoding/json" "fmt" "io" - "net" "net/http" - "strings" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" ) -const ( - protoHTTP = "http" - protoHTTPS = "https" - protoWSS = "wss" - protoWS = "ws" - protoTCP = "tcp" -) - // Client is an HTTP client implementation type Client struct { rpcURL string // the remote RPC URL of the node @@ -38,7 +28,7 @@ func NewClient(rpcURL string) (*Client, error) { c := &Client{ rpcURL: address, - client: DefaultHTTPClient(rpcURL), + client: defaultHTTPClient(rpcURL), } return c, nil @@ -46,11 +36,11 @@ func NewClient(rpcURL string) (*Client, error) { // SendRequest sends a single RPC request to the server func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) { - return sendRequestCommon[types.RPCRequest, types.RPCResponse](ctx, c.client, c.rpcURL, request) + return sendRequestCommon[types.RPCRequest, *types.RPCResponse](ctx, c.client, c.rpcURL, request) } // SendBatch sends a single RPC batch request to the server -func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (*types.RPCResponses, error) { +func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) { return sendRequestCommon[types.RPCRequests, types.RPCResponses](ctx, c.client, c.rpcURL, requests) } @@ -60,7 +50,7 @@ type ( } responseType interface { - types.RPCResponse | types.RPCResponses + *types.RPCResponse | types.RPCResponses } ) @@ -70,7 +60,7 @@ func sendRequestCommon[T requestType, R responseType]( client *http.Client, rpcURL string, request T, -) (*R, error) { +) (R, error) { // Marshal the request requestBytes, err := json.Marshal(request) if err != nil { @@ -114,95 +104,5 @@ func sendRequestCommon[T requestType, R responseType]( return nil, fmt.Errorf("unable to unmarshal response body, %w", err) } - return &response, nil -} - -// isOKStatus returns a boolean indicating if the response -// status code is between 200 and 299 (inclusive) -func isOKStatus(code int) bool { return code >= 200 && code <= 299 } - -// protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp") -// trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "." -func toClientAddrAndParse(remoteAddr string) (string, string, error) { - protocol, address, err := parseRemoteAddr(remoteAddr) - if err != nil { - return "", "", err - } - - // protocol to use for http operations, to support both http and https - var clientProtocol string - // default to http for unknown protocols (ex. tcp) - switch protocol { - case protoHTTP, protoHTTPS, protoWS, protoWSS: - clientProtocol = protocol - default: - clientProtocol = protoHTTP - } - - // replace / with . for http requests (kvstore domain) - trimmedAddress := strings.Replace(address, "/", ".", -1) - - return clientProtocol, trimmedAddress, nil -} - -func toClientAddress(remoteAddr string) (string, error) { - clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr) - if err != nil { - return "", err - } - - return clientProtocol + "://" + trimmedAddress, nil -} - -// network - name of the network (for example, "tcp", "unix") -// s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") -// TODO: Deprecate support for IP:PORT or /path/to/socket -func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { - parts := strings.SplitN(remoteAddr, "://", 2) - var protocol, address string - switch { - case len(parts) == 1: - // default to tcp if nothing specified - protocol, address = protoTCP, remoteAddr - case len(parts) == 2: - protocol, address = parts[0], parts[1] - default: - return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) - } - - return protocol, address, nil -} - -func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) { - protocol, address, err := parseRemoteAddr(remoteAddr) - if err != nil { - return func(_ string, _ string) (net.Conn, error) { - return nil, err - } - } - - // net.Dial doesn't understand http/https, so change it to TCP - switch protocol { - case protoHTTP, protoHTTPS: - protocol = protoTCP - } - - return func(proto, addr string) (net.Conn, error) { - return net.Dial(protocol, address) - } -} - -// DefaultHTTPClient is used to create an http client with some default parameters. -// We overwrite the http.Client.Dial so we can do http over tcp or unix. -// remoteAddr should be fully featured (eg. with tcp:// or unix://) -func DefaultHTTPClient(remoteAddr string) *http.Client { - return &http.Client{ - Transport: &http.Transport{ - // Set to true to prevent GZIP-bomb DoS attacks - DisableCompression: true, - DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { - return makeHTTPDialer(remoteAddr)(network, addr) - }, - }, - } + return response, nil } diff --git a/tm2/pkg/bft/rpc/lib/client/http/client_test.go b/tm2/pkg/bft/rpc/lib/client/http/client_test.go index c24d024abf5..5878e78bacf 100644 --- a/tm2/pkg/bft/rpc/lib/client/http/client_test.go +++ b/tm2/pkg/bft/rpc/lib/client/http/client_test.go @@ -205,9 +205,9 @@ func TestClient_SendBatchRequest(t *testing.T) { resps, err := c.SendBatch(ctx, requests) require.NoError(t, err) - require.Len(t, *resps, len(requests)) + require.Len(t, resps, len(requests)) - for _, resp := range *resps { + for _, resp := range resps { assert.Equal(t, request.ID, resp.ID) assert.Equal(t, request.JSONRPC, resp.JSONRPC) assert.Nil(t, resp.Result) diff --git a/tm2/pkg/bft/rpc/lib/client/http/utils.go b/tm2/pkg/bft/rpc/lib/client/http/utils.go new file mode 100644 index 00000000000..0a5324195db --- /dev/null +++ b/tm2/pkg/bft/rpc/lib/client/http/utils.go @@ -0,0 +1,107 @@ +package http + +import ( + "context" + "fmt" + "net" + "net/http" + "strings" +) + +const ( + protoHTTP = "http" + protoHTTPS = "https" + protoWSS = "wss" + protoWS = "ws" + protoTCP = "tcp" +) + +// DefaultHTTPClient is used to create an http client with some default parameters. +// We overwrite the http.Client.Dial so we can do http over tcp or unix. +// remoteAddr should be fully featured (eg. with tcp:// or unix://) +func defaultHTTPClient(remoteAddr string) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + // Set to true to prevent GZIP-bomb DoS attacks + DisableCompression: true, + DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { + return makeHTTPDialer(remoteAddr)(network, addr) + }, + }, + } +} + +func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) { + protocol, address, err := parseRemoteAddr(remoteAddr) + if err != nil { + return func(_ string, _ string) (net.Conn, error) { + return nil, err + } + } + + // net.Dial doesn't understand http/https, so change it to TCP + switch protocol { + case protoHTTP, protoHTTPS: + protocol = protoTCP + } + + return func(proto, addr string) (net.Conn, error) { + return net.Dial(protocol, address) + } +} + +// protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp") +// trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "." +func toClientAddrAndParse(remoteAddr string) (string, string, error) { + protocol, address, err := parseRemoteAddr(remoteAddr) + if err != nil { + return "", "", err + } + + // protocol to use for http operations, to support both http and https + var clientProtocol string + // default to http for unknown protocols (ex. tcp) + switch protocol { + case protoHTTP, protoHTTPS, protoWS, protoWSS: + clientProtocol = protocol + default: + clientProtocol = protoHTTP + } + + // replace / with . for http requests (kvstore domain) + trimmedAddress := strings.Replace(address, "/", ".", -1) + + return clientProtocol, trimmedAddress, nil +} + +func toClientAddress(remoteAddr string) (string, error) { + clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr) + if err != nil { + return "", err + } + + return clientProtocol + "://" + trimmedAddress, nil +} + +// network - name of the network (for example, "tcp", "unix") +// s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") +// TODO: Deprecate support for IP:PORT or /path/to/socket +func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { + parts := strings.SplitN(remoteAddr, "://", 2) + var protocol, address string + switch { + case len(parts) == 1: + // default to tcp if nothing specified + protocol, address = protoTCP, remoteAddr + case len(parts) == 2: + protocol, address = parts[0], parts[1] + default: + return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) + } + + return protocol, address, nil +} + +// isOKStatus returns a boolean indicating if the response +// status code is between 200 and 299 (inclusive) +func isOKStatus(code int) bool { return code >= 200 && code <= 299 } diff --git a/tm2/pkg/bft/rpc/lib/client/uri_client.go b/tm2/pkg/bft/rpc/lib/client/uri_client.go deleted file mode 100644 index c02eee12d91..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/uri_client.go +++ /dev/null @@ -1,65 +0,0 @@ -package rpcclient - -import ( - "encoding/json" - "fmt" - "io" - "net/http" - - http2 "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" - types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/errors" -) - -// URI takes params as a map -type URIClient struct { - address string - client *http.Client -} - -// The function panics if the provided remote is invalid. -func NewURIClient(remote string) *URIClient { - clientAddress, err := http2.toClientAddress(remote) - if err != nil { - panic(fmt.Sprintf("invalid remote %s: %s", remote, err)) - } - return &URIClient{ - address: clientAddress, - client: http2.DefaultHTTPClient(remote), - } -} - -func (c *URIClient) Call(method string, params map[string]any, result any) error { - values, err := http2.argsToURLValues(params) - if err != nil { - return err - } - // log.Info(Fmt("URI request to %v (%v): %v", c.address, method, values)) - resp, err := c.client.PostForm(c.address+"/"+method, values) - if err != nil { - return err - } - defer resp.Body.Close() //nolint: errcheck - - if !http2.statusOK(resp.StatusCode) { - return errors.New("server at '%s' returned %s", c.address, resp.Status) - } - - responseBytes, err := io.ReadAll(resp.Body) - if err != nil { - return err - } - - var response types.RPCResponse - - err = json.Unmarshal(responseBytes, &response) - if err != nil { - return errors.Wrap(err, "error unmarshalling rpc response") - } - - if response.Error != nil { - return errors.Wrap(response.Error, "response error") - } - - return http2.unmarshalResponseIntoResult(&response, types.JSONRPCStringID(""), result) -} diff --git a/tm2/pkg/bft/rpc/lib/rpc_test.go b/tm2/pkg/bft/rpc/lib/rpc_test.go index 158db4f653a..95fe5e362be 100644 --- a/tm2/pkg/bft/rpc/lib/rpc_test.go +++ b/tm2/pkg/bft/rpc/lib/rpc_test.go @@ -8,10 +8,10 @@ import ( "time" http2 "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" + client "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/uri" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - client "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" server "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" "github.com/gnolang/gno/tm2/pkg/log" @@ -146,7 +146,7 @@ func echoViaHTTP(cl http2.HTTPClient, val string) (string, error) { func TestHexStringArg(t *testing.T) { t.Parallel() - cl := client.NewURIClient(tcpAddr) + cl := client.NewClient(tcpAddr) // should NOT be handled as hex val := "0xabc" got, err := echoViaHTTP(cl, val) @@ -157,7 +157,7 @@ func TestHexStringArg(t *testing.T) { func TestQuotedStringArg(t *testing.T) { t.Parallel() - cl := client.NewURIClient(tcpAddr) + cl := client.NewClient(tcpAddr) // should NOT be unquoted val := "\"abc\"" got, err := echoViaHTTP(cl, val) From 61141c798a470cda291c722a5edb9a8e47af1f6b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Thu, 11 Apr 2024 10:33:26 +0200 Subject: [PATCH 09/26] Drop utils --- tm2/pkg/bft/rpc/lib/client/http/client.go | 100 ++++++++++++++++++++ tm2/pkg/bft/rpc/lib/client/http/utils.go | 107 ---------------------- 2 files changed, 100 insertions(+), 107 deletions(-) delete mode 100644 tm2/pkg/bft/rpc/lib/client/http/utils.go diff --git a/tm2/pkg/bft/rpc/lib/client/http/client.go b/tm2/pkg/bft/rpc/lib/client/http/client.go index 2ed46117d71..cbcb49f4069 100644 --- a/tm2/pkg/bft/rpc/lib/client/http/client.go +++ b/tm2/pkg/bft/rpc/lib/client/http/client.go @@ -6,11 +6,21 @@ import ( "encoding/json" "fmt" "io" + "net" "net/http" + "strings" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" ) +const ( + protoHTTP = "http" + protoHTTPS = "https" + protoWSS = "wss" + protoWS = "ws" + protoTCP = "tcp" +) + // Client is an HTTP client implementation type Client struct { rpcURL string // the remote RPC URL of the node @@ -106,3 +116,93 @@ func sendRequestCommon[T requestType, R responseType]( return response, nil } + +// DefaultHTTPClient is used to create an http client with some default parameters. +// We overwrite the http.Client.Dial so we can do http over tcp or unix. +// remoteAddr should be fully featured (eg. with tcp:// or unix://) +func defaultHTTPClient(remoteAddr string) *http.Client { + return &http.Client{ + Transport: &http.Transport{ + // Set to true to prevent GZIP-bomb DoS attacks + DisableCompression: true, + DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { + return makeHTTPDialer(remoteAddr)(network, addr) + }, + }, + } +} + +func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) { + protocol, address, err := parseRemoteAddr(remoteAddr) + if err != nil { + return func(_ string, _ string) (net.Conn, error) { + return nil, err + } + } + + // net.Dial doesn't understand http/https, so change it to TCP + switch protocol { + case protoHTTP, protoHTTPS: + protocol = protoTCP + } + + return func(proto, addr string) (net.Conn, error) { + return net.Dial(protocol, address) + } +} + +// protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp") +// trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "." +func toClientAddrAndParse(remoteAddr string) (string, string, error) { + protocol, address, err := parseRemoteAddr(remoteAddr) + if err != nil { + return "", "", err + } + + // protocol to use for http operations, to support both http and https + var clientProtocol string + // default to http for unknown protocols (ex. tcp) + switch protocol { + case protoHTTP, protoHTTPS, protoWS, protoWSS: + clientProtocol = protocol + default: + clientProtocol = protoHTTP + } + + // replace / with . for http requests (kvstore domain) + trimmedAddress := strings.Replace(address, "/", ".", -1) + + return clientProtocol, trimmedAddress, nil +} + +func toClientAddress(remoteAddr string) (string, error) { + clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr) + if err != nil { + return "", err + } + + return clientProtocol + "://" + trimmedAddress, nil +} + +// network - name of the network (for example, "tcp", "unix") +// s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") +// TODO: Deprecate support for IP:PORT or /path/to/socket +func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { + parts := strings.SplitN(remoteAddr, "://", 2) + var protocol, address string + switch { + case len(parts) == 1: + // default to tcp if nothing specified + protocol, address = protoTCP, remoteAddr + case len(parts) == 2: + protocol, address = parts[0], parts[1] + default: + return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) + } + + return protocol, address, nil +} + +// isOKStatus returns a boolean indicating if the response +// status code is between 200 and 299 (inclusive) +func isOKStatus(code int) bool { return code >= 200 && code <= 299 } diff --git a/tm2/pkg/bft/rpc/lib/client/http/utils.go b/tm2/pkg/bft/rpc/lib/client/http/utils.go deleted file mode 100644 index 0a5324195db..00000000000 --- a/tm2/pkg/bft/rpc/lib/client/http/utils.go +++ /dev/null @@ -1,107 +0,0 @@ -package http - -import ( - "context" - "fmt" - "net" - "net/http" - "strings" -) - -const ( - protoHTTP = "http" - protoHTTPS = "https" - protoWSS = "wss" - protoWS = "ws" - protoTCP = "tcp" -) - -// DefaultHTTPClient is used to create an http client with some default parameters. -// We overwrite the http.Client.Dial so we can do http over tcp or unix. -// remoteAddr should be fully featured (eg. with tcp:// or unix://) -func defaultHTTPClient(remoteAddr string) *http.Client { - return &http.Client{ - Transport: &http.Transport{ - // Set to true to prevent GZIP-bomb DoS attacks - DisableCompression: true, - DialContext: func(_ context.Context, network, addr string) (net.Conn, error) { - return makeHTTPDialer(remoteAddr)(network, addr) - }, - }, - } -} - -func makeHTTPDialer(remoteAddr string) func(string, string) (net.Conn, error) { - protocol, address, err := parseRemoteAddr(remoteAddr) - if err != nil { - return func(_ string, _ string) (net.Conn, error) { - return nil, err - } - } - - // net.Dial doesn't understand http/https, so change it to TCP - switch protocol { - case protoHTTP, protoHTTPS: - protocol = protoTCP - } - - return func(proto, addr string) (net.Conn, error) { - return net.Dial(protocol, address) - } -} - -// protocol - client's protocol (for example, "http", "https", "wss", "ws", "tcp") -// trimmedS - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") with "/" replaced with "." -func toClientAddrAndParse(remoteAddr string) (string, string, error) { - protocol, address, err := parseRemoteAddr(remoteAddr) - if err != nil { - return "", "", err - } - - // protocol to use for http operations, to support both http and https - var clientProtocol string - // default to http for unknown protocols (ex. tcp) - switch protocol { - case protoHTTP, protoHTTPS, protoWS, protoWSS: - clientProtocol = protocol - default: - clientProtocol = protoHTTP - } - - // replace / with . for http requests (kvstore domain) - trimmedAddress := strings.Replace(address, "/", ".", -1) - - return clientProtocol, trimmedAddress, nil -} - -func toClientAddress(remoteAddr string) (string, error) { - clientProtocol, trimmedAddress, err := toClientAddrAndParse(remoteAddr) - if err != nil { - return "", err - } - - return clientProtocol + "://" + trimmedAddress, nil -} - -// network - name of the network (for example, "tcp", "unix") -// s - rest of the address (for example, "192.0.2.1:25", "[2001:db8::1]:80") -// TODO: Deprecate support for IP:PORT or /path/to/socket -func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { - parts := strings.SplitN(remoteAddr, "://", 2) - var protocol, address string - switch { - case len(parts) == 1: - // default to tcp if nothing specified - protocol, address = protoTCP, remoteAddr - case len(parts) == 2: - protocol, address = parts[0], parts[1] - default: - return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) - } - - return protocol, address, nil -} - -// isOKStatus returns a boolean indicating if the response -// status code is between 200 and 299 (inclusive) -func isOKStatus(code int) bool { return code >= 200 && code <= 299 } From 1a418b9c7ff6fc549bc4e85f73c486b5d1b7350e Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 12 Apr 2024 17:10:34 +0200 Subject: [PATCH 10/26] Clean up rpc/client --- go.mod | 2 + go.sum | 2 + tm2/pkg/bft/rpc/client/base.go | 226 ------- tm2/pkg/bft/rpc/client/client.go | 350 ++++++++++ tm2/pkg/bft/rpc/client/client_test.go | 674 +++++++++++++++++++ tm2/pkg/bft/rpc/client/doc.go | 18 + tm2/pkg/bft/rpc/client/examples_test.go | 127 ---- tm2/pkg/bft/rpc/client/helpers.go | 49 -- tm2/pkg/bft/rpc/client/helpers_test.go | 87 --- tm2/pkg/bft/rpc/client/http.go | 109 --- tm2/pkg/bft/rpc/client/interface.go | 95 --- tm2/pkg/bft/rpc/client/local.go | 10 - tm2/pkg/bft/rpc/client/main_test.go | 28 - tm2/pkg/bft/rpc/client/mock/abci.go | 209 ------ tm2/pkg/bft/rpc/client/mock/abci_test.go | 191 ------ tm2/pkg/bft/rpc/client/mock/client.go | 152 ----- tm2/pkg/bft/rpc/client/mock/status.go | 52 -- tm2/pkg/bft/rpc/client/mock/status_test.go | 48 -- tm2/pkg/bft/rpc/client/mock_test.go | 33 + tm2/pkg/bft/rpc/client/options.go | 12 + tm2/pkg/bft/rpc/client/rpc_test.go | 597 ---------------- tm2/pkg/bft/rpc/client/types.go | 74 ++ tm2/pkg/bft/rpc/client/ws.go | 31 - tm2/pkg/bft/rpc/lib/client/http/client.go | 39 +- tm2/pkg/bft/rpc/lib/client/ws/client.go | 67 +- tm2/pkg/bft/rpc/lib/client/ws/client_test.go | 6 +- tm2/pkg/bft/rpc/lib/test/main.go | 42 -- tm2/pkg/bft/rpc/test/helpers.go | 145 ---- 28 files changed, 1249 insertions(+), 2226 deletions(-) delete mode 100644 tm2/pkg/bft/rpc/client/base.go create mode 100644 tm2/pkg/bft/rpc/client/client.go create mode 100644 tm2/pkg/bft/rpc/client/client_test.go create mode 100644 tm2/pkg/bft/rpc/client/doc.go delete mode 100644 tm2/pkg/bft/rpc/client/examples_test.go delete mode 100644 tm2/pkg/bft/rpc/client/helpers.go delete mode 100644 tm2/pkg/bft/rpc/client/helpers_test.go delete mode 100644 tm2/pkg/bft/rpc/client/http.go delete mode 100644 tm2/pkg/bft/rpc/client/interface.go delete mode 100644 tm2/pkg/bft/rpc/client/main_test.go delete mode 100644 tm2/pkg/bft/rpc/client/mock/abci.go delete mode 100644 tm2/pkg/bft/rpc/client/mock/abci_test.go delete mode 100644 tm2/pkg/bft/rpc/client/mock/client.go delete mode 100644 tm2/pkg/bft/rpc/client/mock/status.go delete mode 100644 tm2/pkg/bft/rpc/client/mock/status_test.go create mode 100644 tm2/pkg/bft/rpc/client/mock_test.go create mode 100644 tm2/pkg/bft/rpc/client/options.go delete mode 100644 tm2/pkg/bft/rpc/client/rpc_test.go delete mode 100644 tm2/pkg/bft/rpc/client/ws.go delete mode 100644 tm2/pkg/bft/rpc/lib/test/main.go delete mode 100644 tm2/pkg/bft/rpc/test/helpers.go diff --git a/go.mod b/go.mod index c6983d10b4f..cefc56df62c 100644 --- a/go.mod +++ b/go.mod @@ -45,6 +45,8 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) +require github.com/rs/xid v1.5.0 // indirect + require ( github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/gdamore/encoding v1.0.0 // indirect diff --git a/go.sum b/go.sum index b71747f5d32..9da3fc83d22 100644 --- a/go.sum +++ b/go.sum @@ -147,6 +147,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/tm2/pkg/bft/rpc/client/base.go b/tm2/pkg/bft/rpc/client/base.go deleted file mode 100644 index 5de961bd301..00000000000 --- a/tm2/pkg/bft/rpc/client/base.go +++ /dev/null @@ -1,226 +0,0 @@ -package client - -import ( - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/errors" -) - -var _ Client = (*baseRPCClient)(nil) - -func (c *baseRPCClient) Status() (*ctypes.ResultStatus, error) { - result := new(ctypes.ResultStatus) - err := c.caller.Call("status", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "Status") - } - return result, nil -} - -func (c *baseRPCClient) ABCIInfo() (*ctypes.ResultABCIInfo, error) { - result := new(ctypes.ResultABCIInfo) - err := c.caller.Call("abci_info", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "ABCIInfo") - } - return result, nil -} - -func (c *baseRPCClient) ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) { - return c.ABCIQueryWithOptions(path, data, DefaultABCIQueryOptions) -} - -func (c *baseRPCClient) ABCIQueryWithOptions(path string, data []byte, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { - result := new(ctypes.ResultABCIQuery) - err := c.caller.Call("abci_query", - map[string]any{"path": path, "data": data, "height": opts.Height, "prove": opts.Prove}, - result) - if err != nil { - return nil, errors.Wrap(err, "ABCIQuery") - } - return result, nil -} - -func (c *baseRPCClient) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - result := new(ctypes.ResultBroadcastTxCommit) - err := c.caller.Call("broadcast_tx_commit", map[string]any{"tx": tx}, result) - if err != nil { - return nil, errors.Wrap(err, "broadcast_tx_commit") - } - return result, nil -} - -func (c *baseRPCClient) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - return c.broadcastTX("broadcast_tx_async", tx) -} - -func (c *baseRPCClient) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - return c.broadcastTX("broadcast_tx_sync", tx) -} - -func (c *baseRPCClient) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - result := new(ctypes.ResultBroadcastTx) - err := c.caller.Call(route, map[string]any{"tx": tx}, result) - if err != nil { - return nil, errors.Wrap(err, route) - } - return result, nil -} - -func (c *baseRPCClient) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) { - result := new(ctypes.ResultUnconfirmedTxs) - err := c.caller.Call("unconfirmed_txs", map[string]any{"limit": limit}, result) - if err != nil { - return nil, errors.Wrap(err, "unconfirmed_txs") - } - return result, nil -} - -func (c *baseRPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { - result := new(ctypes.ResultUnconfirmedTxs) - err := c.caller.Call("num_unconfirmed_txs", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "num_unconfirmed_txs") - } - return result, nil -} - -func (c *baseRPCClient) NetInfo() (*ctypes.ResultNetInfo, error) { - result := new(ctypes.ResultNetInfo) - err := c.caller.Call("net_info", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "NetInfo") - } - return result, nil -} - -func (c *baseRPCClient) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { - result := new(ctypes.ResultDumpConsensusState) - err := c.caller.Call("dump_consensus_state", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "DumpConsensusState") - } - return result, nil -} - -func (c *baseRPCClient) ConsensusState() (*ctypes.ResultConsensusState, error) { - result := new(ctypes.ResultConsensusState) - err := c.caller.Call("consensus_state", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "ConsensusState") - } - return result, nil -} - -func (c *baseRPCClient) ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, error) { - result := new(ctypes.ResultConsensusParams) - - if err := c.caller.Call( - "consensus_params", - map[string]any{ - "height": height, - }, - result, - ); err != nil { - return nil, errors.Wrap(err, "ConsensusParams") - } - - return result, nil -} - -func (c *baseRPCClient) Health() (*ctypes.ResultHealth, error) { - result := new(ctypes.ResultHealth) - err := c.caller.Call("health", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "Health") - } - return result, nil -} - -func (c *baseRPCClient) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { - result := new(ctypes.ResultBlockchainInfo) - err := c.caller.Call("blockchain", - map[string]any{"minHeight": minHeight, "maxHeight": maxHeight}, - result) - if err != nil { - return nil, errors.Wrap(err, "BlockchainInfo") - } - return result, nil -} - -func (c *baseRPCClient) Genesis() (*ctypes.ResultGenesis, error) { - result := new(ctypes.ResultGenesis) - err := c.caller.Call("genesis", map[string]any{}, result) - if err != nil { - return nil, errors.Wrap(err, "Genesis") - } - return result, nil -} - -func (c *baseRPCClient) Block(height *int64) (*ctypes.ResultBlock, error) { - result := new(ctypes.ResultBlock) - err := c.caller.Call("block", map[string]any{"height": height}, result) - if err != nil { - return nil, errors.Wrap(err, "Block") - } - return result, nil -} - -func (c *baseRPCClient) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { - result := new(ctypes.ResultBlockResults) - err := c.caller.Call("block_results", map[string]any{"height": height}, result) - if err != nil { - return nil, errors.Wrap(err, "Block Result") - } - return result, nil -} - -func (c *baseRPCClient) Commit(height *int64) (*ctypes.ResultCommit, error) { - result := new(ctypes.ResultCommit) - err := c.caller.Call("commit", map[string]any{"height": height}, result) - if err != nil { - return nil, errors.Wrap(err, "Commit") - } - return result, nil -} - -func (c *baseRPCClient) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { - result := new(ctypes.ResultTx) - params := map[string]any{ - "hash": hash, - "prove": prove, - } - err := c.caller.Call("tx", params, result) - if err != nil { - return nil, errors.Wrap(err, "Tx") - } - return result, nil -} - -func (c *baseRPCClient) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { - result := new(ctypes.ResultTxSearch) - params := map[string]any{ - "query": query, - "prove": prove, - "page": page, - "per_page": perPage, - } - err := c.caller.Call("tx_search", params, result) - if err != nil { - return nil, errors.Wrap(err, "TxSearch") - } - return result, nil -} - -func (c *baseRPCClient) Validators(height *int64) (*ctypes.ResultValidators, error) { - result := new(ctypes.ResultValidators) - params := map[string]any{} - if height != nil { - params["height"] = height - } - err := c.caller.Call("validators", params, result) - if err != nil { - return nil, errors.Wrap(err, "Validators") - } - return result, nil -} diff --git a/tm2/pkg/bft/rpc/client/client.go b/tm2/pkg/bft/rpc/client/client.go new file mode 100644 index 00000000000..6ff5450192a --- /dev/null +++ b/tm2/pkg/bft/rpc/client/client.go @@ -0,0 +1,350 @@ +package client + +import ( + "context" + "fmt" + "time" + + "github.com/gnolang/gno/tm2/pkg/amino" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/batch" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/ws" + rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/rs/xid" +) + +const defaultTimeout = 60 * time.Second + +const ( + statusMethod = "status" + abciInfoMethod = "abci_info" + abciQueryMethod = "abci_query" + broadcastTxCommitMethod = "broadcast_tx_commit" + broadcastTxAsyncMethod = "broadcast_tx_async" + broadcastTxSyncMethod = "broadcast_tx_sync" + unconfirmedTxsMethod = "unconfirmed_txs" + numUnconfirmedTxsMethod = "num_unconfirmed_txs" + netInfoMethod = "net_info" + dumpConsensusStateMethod = "dump_consensus_state" + consensusStateMethod = "consensus_state" + consensusParamsMethod = "consensus_params" + healthMethod = "health" + blockchainMethod = "blockchain" + genesisMethod = "genesis" + blockMethod = "block" + blockResultsMethod = "block_results" + commitMethod = "commit" + validatorsMethod = "validators" +) + +// RPCClient encompasses common RPC client methods +type RPCClient struct { + requestTimeout time.Duration + + caller rpcclient.Client +} + +// NewRPCClient creates a new RPC client instance with the given caller +func NewRPCClient(caller rpcclient.Client, opts ...Option) *RPCClient { + c := &RPCClient{ + requestTimeout: defaultTimeout, + caller: caller, + } + + for _, opt := range opts { + opt(c) + } + + return c +} + +// NewHTTPClient takes a remote endpoint in the form ://:, +// and returns an HTTP client that communicates with a Tendermint node over +// JSON RPC. +// +// 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 +func NewHTTPClient(rpcURL string) (*RPCClient, error) { + httpClient, err := http.NewClient(rpcURL) + if err != nil { + return nil, err + } + + return NewRPCClient(httpClient), nil +} + +// NewWSClient takes a remote endpoint in the form ://:, +// and returns a WS client that communicates with a Tendermint node over +// WS connection. +// +// Request batching is available for JSON RPC requests over WS, which conforms to +// the JSON RPC specification (https://www.jsonrpc.org/specification#batch). See +// the example for more details +func NewWSClient(rpcURL string) (*RPCClient, error) { + wsClient, err := ws.NewClient(rpcURL) + if err != nil { + return nil, err + } + + return NewRPCClient(wsClient), nil +} + +// NewBatch creates a new RPC batch +func (c *RPCClient) NewBatch() *batch.Batch { + return batch.NewBatch(c.caller) +} + +func (c *RPCClient) Status() (*ctypes.ResultStatus, error) { + return sendRequestCommon[ctypes.ResultStatus]( + c.caller, + c.requestTimeout, + statusMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) ABCIInfo() (*ctypes.ResultABCIInfo, error) { + return sendRequestCommon[ctypes.ResultABCIInfo]( + c.caller, + c.requestTimeout, + abciInfoMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) { + return c.ABCIQueryWithOptions(path, data, DefaultABCIQueryOptions) +} + +func (c *RPCClient) ABCIQueryWithOptions(path string, data []byte, opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { + return sendRequestCommon[ctypes.ResultABCIQuery]( + c.caller, + c.requestTimeout, + abciQueryMethod, + map[string]any{ + "path": path, + "data": data, + "height": opts.Height, + "prove": opts.Prove, + }, + ) +} + +func (c *RPCClient) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { + return sendRequestCommon[ctypes.ResultBroadcastTxCommit]( + c.caller, + c.requestTimeout, + broadcastTxCommitMethod, + map[string]any{"tx": tx}, + ) +} + +func (c *RPCClient) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return c.broadcastTX(broadcastTxAsyncMethod, tx) +} + +func (c *RPCClient) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return c.broadcastTX(broadcastTxSyncMethod, tx) +} + +func (c *RPCClient) broadcastTX(route string, tx types.Tx) (*ctypes.ResultBroadcastTx, error) { + return sendRequestCommon[ctypes.ResultBroadcastTx]( + c.caller, + c.requestTimeout, + route, + map[string]any{"tx": tx}, + ) +} + +func (c *RPCClient) UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) { + return sendRequestCommon[ctypes.ResultUnconfirmedTxs]( + c.caller, + c.requestTimeout, + unconfirmedTxsMethod, + map[string]any{"limit": limit}, + ) +} + +func (c *RPCClient) NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) { + return sendRequestCommon[ctypes.ResultUnconfirmedTxs]( + c.caller, + c.requestTimeout, + numUnconfirmedTxsMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) NetInfo() (*ctypes.ResultNetInfo, error) { + return sendRequestCommon[ctypes.ResultNetInfo]( + c.caller, + c.requestTimeout, + netInfoMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { + return sendRequestCommon[ctypes.ResultDumpConsensusState]( + c.caller, + c.requestTimeout, + dumpConsensusStateMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) ConsensusState() (*ctypes.ResultConsensusState, error) { + return sendRequestCommon[ctypes.ResultConsensusState]( + c.caller, + c.requestTimeout, + consensusStateMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, error) { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + return sendRequestCommon[ctypes.ResultConsensusParams]( + c.caller, + c.requestTimeout, + consensusParamsMethod, + params, + ) +} + +func (c *RPCClient) Health() (*ctypes.ResultHealth, error) { + return sendRequestCommon[ctypes.ResultHealth]( + c.caller, + c.requestTimeout, + healthMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { + return sendRequestCommon[ctypes.ResultBlockchainInfo]( + c.caller, + c.requestTimeout, + blockchainMethod, + map[string]any{ + "minHeight": minHeight, + "maxHeight": maxHeight, + }, + ) +} + +func (c *RPCClient) Genesis() (*ctypes.ResultGenesis, error) { + return sendRequestCommon[ctypes.ResultGenesis]( + c.caller, + c.requestTimeout, + genesisMethod, + map[string]any{}, + ) +} + +func (c *RPCClient) Block(height *int64) (*ctypes.ResultBlock, error) { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + return sendRequestCommon[ctypes.ResultBlock]( + c.caller, + c.requestTimeout, + blockMethod, + params, + ) +} + +func (c *RPCClient) BlockResults(height *int64) (*ctypes.ResultBlockResults, error) { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + return sendRequestCommon[ctypes.ResultBlockResults]( + c.caller, + c.requestTimeout, + blockResultsMethod, + params, + ) +} + +func (c *RPCClient) Commit(height *int64) (*ctypes.ResultCommit, error) { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + return sendRequestCommon[ctypes.ResultCommit]( + c.caller, + c.requestTimeout, + commitMethod, + params, + ) +} + +func (c *RPCClient) Validators(height *int64) (*ctypes.ResultValidators, error) { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + return sendRequestCommon[ctypes.ResultValidators]( + c.caller, + c.requestTimeout, + validatorsMethod, + params, + ) +} + +// sendRequestCommon is the common request creation, sending, and parsing middleware +func sendRequestCommon[T any]( + caller rpcclient.Client, + timeout time.Duration, + method string, + params map[string]any, +) (*T, error) { + // Prepare the RPC request + id := rpctypes.JSONRPCStringID(xid.New().String()) + request, err := rpctypes.MapToRequest(id, method, params) + if err != nil { + return nil, err + } + + // Send the request + ctx, cancelFn := context.WithTimeout(context.Background(), timeout) + defer cancelFn() + + response, err := caller.SendRequest(ctx, request) + if err != nil { + return nil, fmt.Errorf("unable to call RPC method %s, %w", method, err) + } + + // Parse the response + if response.Error != nil { + return nil, response.Error + } + + // Unmarshal the RPC response + return unmarshalResponseBytes[T](response.Result) +} + +// unmarshalResponseBytes Amino JSON-unmarshals the RPC response data +func unmarshalResponseBytes[T any](responseBytes []byte) (*T, error) { + var result T + + // Amino JSON-unmarshal the RPC response data + if err := amino.UnmarshalJSON(responseBytes, &result); err != nil { + return nil, fmt.Errorf("unable to unmarshal response bytes, %w", err) + } + + return &result, nil +} diff --git a/tm2/pkg/bft/rpc/client/client_test.go b/tm2/pkg/bft/rpc/client/client_test.go new file mode 100644 index 00000000000..299b0fee01c --- /dev/null +++ b/tm2/pkg/bft/rpc/client/client_test.go @@ -0,0 +1,674 @@ +package client + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + cstypes "github.com/gnolang/gno/tm2/pkg/bft/consensus/types" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateMockRequestClient generates a single RPC request mock client +func generateMockRequestClient( + t *testing.T, + method string, + verifyParamsFn func(*testing.T, map[string]any), + responseData any, +) *mockClient { + t.Helper() + + return &mockClient{ + sendRequestFn: func( + _ context.Context, + request types.RPCRequest, + ) (*types.RPCResponse, error) { + // Validate the request + require.Equal(t, "2.0", request.JSONRPC) + require.NotNil(t, request.ID) + require.Equal(t, request.Method, method) + + // Validate the params + var params map[string]any + require.NoError(t, json.Unmarshal(request.Params, ¶ms)) + + verifyParamsFn(t, params) + + // Prepare the result + result, err := amino.MarshalJSON(responseData) + require.NoError(t, err) + + // Prepare the response + response := &types.RPCResponse{ + JSONRPC: "2.0", + ID: request.ID, + Result: result, + Error: nil, + } + + return response, nil + }, + } +} + +func TestRPCClient_Status(t *testing.T) { + t.Parallel() + + var ( + expectedStatus = &ctypes.ResultStatus{ + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + statusMethod, + verifyFn, + expectedStatus, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the status + status, err := c.Status() + require.NoError(t, err) + + assert.Equal(t, expectedStatus, status) +} + +func TestRPCClient_ABCIInfo(t *testing.T) { + t.Parallel() + + var ( + expectedInfo = &ctypes.ResultABCIInfo{ + Response: abci.ResponseInfo{ + LastBlockAppHash: []byte("dummy"), + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + abciInfoMethod, + verifyFn, + expectedInfo, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the info + info, err := c.ABCIInfo() + require.NoError(t, err) + + assert.Equal(t, expectedInfo, info) +} + +func TestRPCClient_ABCIQuery(t *testing.T) { + t.Parallel() + + var ( + path = "path" + data = []byte("data") + opts = DefaultABCIQueryOptions + + expectedQuery = &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte("dummy"), + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, path, params["path"]) + assert.Equal(t, base64.StdEncoding.EncodeToString(data), params["data"]) + assert.Equal(t, fmt.Sprintf("%d", opts.Height), params["height"]) + assert.Equal(t, opts.Prove, params["prove"]) + } + + mockClient = generateMockRequestClient( + t, + abciQueryMethod, + verifyFn, + expectedQuery, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the query + query, err := c.ABCIQuery(path, data) + require.NoError(t, err) + + assert.Equal(t, expectedQuery, query) +} + +func TestRPCClient_BroadcastTxCommit(t *testing.T) { + t.Parallel() + + var ( + tx = []byte("tx") + + expectedTxCommit = &ctypes.ResultBroadcastTxCommit{ + Hash: []byte("dummy"), + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, base64.StdEncoding.EncodeToString(tx), params["tx"]) + } + + mockClient = generateMockRequestClient( + t, + broadcastTxCommitMethod, + verifyFn, + expectedTxCommit, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the broadcast + txCommit, err := c.BroadcastTxCommit(tx) + require.NoError(t, err) + + assert.Equal(t, expectedTxCommit, txCommit) +} + +func TestRPCClient_BroadcastTxAsync(t *testing.T) { + t.Parallel() + + var ( + tx = []byte("tx") + + expectedTxBroadcast = &ctypes.ResultBroadcastTx{ + Hash: []byte("dummy"), + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, base64.StdEncoding.EncodeToString(tx), params["tx"]) + } + + mockClient = generateMockRequestClient( + t, + broadcastTxAsyncMethod, + verifyFn, + expectedTxBroadcast, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the broadcast + txAsync, err := c.BroadcastTxAsync(tx) + require.NoError(t, err) + + assert.Equal(t, expectedTxBroadcast, txAsync) +} + +func TestRPCClient_BroadcastTxSync(t *testing.T) { + t.Parallel() + + var ( + tx = []byte("tx") + + expectedTxBroadcast = &ctypes.ResultBroadcastTx{ + Hash: []byte("dummy"), + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, base64.StdEncoding.EncodeToString(tx), params["tx"]) + } + + mockClient = generateMockRequestClient( + t, + broadcastTxSyncMethod, + verifyFn, + expectedTxBroadcast, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the broadcast + txSync, err := c.BroadcastTxSync(tx) + require.NoError(t, err) + + assert.Equal(t, expectedTxBroadcast, txSync) +} + +func TestRPCClient_UnconfirmedTxs(t *testing.T) { + t.Parallel() + + var ( + limit = 10 + + expectedResult = &ctypes.ResultUnconfirmedTxs{ + Count: 10, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, fmt.Sprintf("%d", limit), params["limit"]) + } + + mockClient = generateMockRequestClient( + t, + unconfirmedTxsMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.UnconfirmedTxs(limit) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_NumUnconfirmedTxs(t *testing.T) { + t.Parallel() + + var ( + expectedResult = &ctypes.ResultUnconfirmedTxs{ + Count: 10, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + numUnconfirmedTxsMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.NumUnconfirmedTxs() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_NetInfo(t *testing.T) { + t.Parallel() + + var ( + expectedResult = &ctypes.ResultNetInfo{ + NPeers: 10, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + netInfoMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.NetInfo() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_DumpConsensusState(t *testing.T) { + t.Parallel() + + var ( + expectedResult = &ctypes.ResultDumpConsensusState{ + RoundState: &cstypes.RoundState{ + Round: 10, + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + dumpConsensusStateMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.DumpConsensusState() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_ConsensusState(t *testing.T) { + t.Parallel() + + var ( + expectedResult = &ctypes.ResultConsensusState{ + RoundState: cstypes.RoundStateSimple{ + ProposalBlockHash: []byte("dummy"), + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + consensusStateMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.ConsensusState() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_ConsensusParams(t *testing.T) { + t.Parallel() + + var ( + blockHeight = int64(10) + + expectedResult = &ctypes.ResultConsensusParams{ + BlockHeight: blockHeight, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, fmt.Sprintf("%d", blockHeight), params["height"]) + } + + mockClient = generateMockRequestClient( + t, + consensusParamsMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.ConsensusParams(&blockHeight) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_Health(t *testing.T) { + t.Parallel() + + var ( + expectedResult = &ctypes.ResultHealth{} + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + healthMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.Health() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_BlockchainInfo(t *testing.T) { + t.Parallel() + + var ( + minHeight = int64(5) + maxHeight = int64(10) + + expectedResult = &ctypes.ResultBlockchainInfo{ + LastHeight: 100, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, fmt.Sprintf("%d", minHeight), params["minHeight"]) + assert.Equal(t, fmt.Sprintf("%d", maxHeight), params["maxHeight"]) + } + + mockClient = generateMockRequestClient( + t, + blockchainMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.BlockchainInfo(minHeight, maxHeight) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_Genesis(t *testing.T) { + t.Parallel() + + var ( + expectedResult = &ctypes.ResultGenesis{ + Genesis: &bfttypes.GenesisDoc{ + ChainID: "dummy", + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestClient( + t, + genesisMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.Genesis() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_Block(t *testing.T) { + t.Parallel() + + var ( + height = int64(10) + + expectedResult = &ctypes.ResultBlock{ + BlockMeta: &bfttypes.BlockMeta{ + Header: bfttypes.Header{ + Height: height, + }, + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) + } + + mockClient = generateMockRequestClient( + t, + blockMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.Block(&height) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_BlockResults(t *testing.T) { + t.Parallel() + + var ( + height = int64(10) + + expectedResult = &ctypes.ResultBlockResults{ + Height: height, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) + } + + mockClient = generateMockRequestClient( + t, + blockResultsMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.BlockResults(&height) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_Commit(t *testing.T) { + t.Parallel() + + var ( + height = int64(10) + + expectedResult = &ctypes.ResultCommit{ + CanonicalCommit: true, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) + } + + mockClient = generateMockRequestClient( + t, + commitMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.Commit(&height) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} + +func TestRPCClient_Validators(t *testing.T) { + t.Parallel() + + var ( + height = int64(10) + + expectedResult = &ctypes.ResultValidators{ + BlockHeight: height, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) + } + + mockClient = generateMockRequestClient( + t, + validatorsMethod, + verifyFn, + expectedResult, + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Get the result + result, err := c.Validators(&height) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) +} diff --git a/tm2/pkg/bft/rpc/client/doc.go b/tm2/pkg/bft/rpc/client/doc.go new file mode 100644 index 00000000000..a243dea1046 --- /dev/null +++ b/tm2/pkg/bft/rpc/client/doc.go @@ -0,0 +1,18 @@ +// Package client provides a general purpose interface (Client) for connecting +// to a tendermint node, as well as higher-level functionality. +// +// The main implementation for production code is client.HTTP, which +// connects via http to the jsonrpc interface of the tendermint node. +// +// For connecting to a node running in the same process (eg. when +// compiling the abci app in the same process), you can use the client.Local +// implementation. +// +// For mocking out server responses during testing to see behavior for +// arbitrary return values, use the mock package. +// +// In addition to the Client interface, which should be used externally +// for maximum flexibility and testability, and two implementations, +// this package also provides helper functions that work on any Client +// implementation. +package client diff --git a/tm2/pkg/bft/rpc/client/examples_test.go b/tm2/pkg/bft/rpc/client/examples_test.go deleted file mode 100644 index b494c229d0e..00000000000 --- a/tm2/pkg/bft/rpc/client/examples_test.go +++ /dev/null @@ -1,127 +0,0 @@ -package client_test - -import ( - "bytes" - "context" - "fmt" - - "github.com/gnolang/gno/tm2/pkg/bft/abci/example/kvstore" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - rpctest "github.com/gnolang/gno/tm2/pkg/bft/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) - - // 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) - - // 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 rpcBatch - batch := c.NewBatch() - - // Queue up our transactions - for _, tx := range txs { - if _, err := batch.BroadcastTxCommit(tx); err != nil { - panic(err) - } - } - - // Send the rpcBatch of 2 transactions - if _, err := batch.Send(context.Background()); err != nil { - panic(err) - } - - // Now let's query for the original results as a rpcBatch - 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(context.Background()) - 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/tm2/pkg/bft/rpc/client/helpers.go b/tm2/pkg/bft/rpc/client/helpers.go deleted file mode 100644 index a3299909f82..00000000000 --- a/tm2/pkg/bft/rpc/client/helpers.go +++ /dev/null @@ -1,49 +0,0 @@ -package client - -import ( - "time" - - "github.com/gnolang/gno/tm2/pkg/errors" -) - -// Waiter is informed of current height, decided whether to quit early -type Waiter func(delta int64) (abort error) - -// DefaultWaitStrategy is the standard backoff algorithm, -// but you can plug in another one -func DefaultWaitStrategy(delta int64) (abort error) { - if delta > 10 { - return errors.New("waiting for %d blocks... aborting", delta) - } else if delta > 0 { - // estimate of wait time.... - // wait half a second for the next block (in progress) - // plus one second for every full block - delay := time.Duration(delta-1)*time.Second + 500*time.Millisecond - time.Sleep(delay) - } - return nil -} - -// Wait for height will poll status at reasonable intervals until -// the block at the given height is available. -// -// If waiter is nil, we use DefaultWaitStrategy, but you can also -// provide your own implementation -func WaitForHeight(c StatusClient, h int64, waiter Waiter) error { - if waiter == nil { - waiter = DefaultWaitStrategy - } - delta := int64(1) - for delta > 0 { - s, err := c.Status() - if err != nil { - return err - } - delta = h - s.SyncInfo.LatestBlockHeight - // wait for the time, or abort early - if err := waiter(delta); err != nil { - return err - } - } - return nil -} diff --git a/tm2/pkg/bft/rpc/client/helpers_test.go b/tm2/pkg/bft/rpc/client/helpers_test.go deleted file mode 100644 index 4d0b54c2358..00000000000 --- a/tm2/pkg/bft/rpc/client/helpers_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package client_test - -import ( - "errors" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client/mock" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/random" -) - -func TestWaitForHeight(t *testing.T) { - t.Parallel() - - assert, require := assert.New(t), require.New(t) - - // test with error result - immediate failure - m := &mock.StatusMock{ - Call: mock.Call{ - Error: errors.New("bye"), - }, - } - r := mock.NewStatusRecorder(m) - - // connection failure always leads to error - err := client.WaitForHeight(r, 8, nil) - require.NotNil(err) - require.Equal("bye", err.Error()) - // we called status once to check - require.Equal(1, len(r.Calls)) - - // now set current block height to 10 - m.Call = mock.Call{ - Response: &ctypes.ResultStatus{SyncInfo: ctypes.SyncInfo{LatestBlockHeight: 10}}, - } - - // we will not wait for more than 10 blocks - err = client.WaitForHeight(r, 40, nil) - require.NotNil(err) - require.True(strings.Contains(err.Error(), "aborting")) - // we called status once more to check - require.Equal(2, len(r.Calls)) - - // waiting for the past returns immediately - err = client.WaitForHeight(r, 5, nil) - require.Nil(err) - // we called status once more to check - require.Equal(3, len(r.Calls)) - - // since we can't update in a background goroutine (test --race) - // we use the callback to update the status height - myWaiter := func(delta int64) error { - // update the height for the next call - m.Call.Response = &ctypes.ResultStatus{SyncInfo: ctypes.SyncInfo{LatestBlockHeight: 15}} - return client.DefaultWaitStrategy(delta) - } - - // we wait for a few blocks - err = client.WaitForHeight(r, 12, myWaiter) - require.Nil(err) - // we called status once to check - require.Equal(5, len(r.Calls)) - - pre := r.Calls[3] - require.Nil(pre.Error) - prer, ok := pre.Response.(*ctypes.ResultStatus) - require.True(ok) - assert.Equal(int64(10), prer.SyncInfo.LatestBlockHeight) - - post := r.Calls[4] - require.Nil(post.Error) - postr, ok := post.Response.(*ctypes.ResultStatus) - require.True(ok) - assert.Equal(int64(15), postr.SyncInfo.LatestBlockHeight) -} - -// MakeTxKV returns a text transaction, allong with expected key, value pair -func MakeTxKV() ([]byte, []byte, []byte) { - k := []byte(random.RandStr(8)) - v := []byte(random.RandStr(8)) - return k, v, append(k, append([]byte("="), v...)...) -} diff --git a/tm2/pkg/bft/rpc/client/http.go b/tm2/pkg/bft/rpc/client/http.go deleted file mode 100644 index 8efc260091d..00000000000 --- a/tm2/pkg/bft/rpc/client/http.go +++ /dev/null @@ -1,109 +0,0 @@ -package client - -import ( - "context" - "net/http" - - rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/batch" - http2 "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" -) - -/* -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 -(Local), or when you want to mock out the server for test code (mock). - -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 *http2.Client - - *baseRPCClient -} - -// Batch 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 Batch struct { - rpcBatch *rpcclient.Batch - *baseRPCClient -} - -// baseRPCClient implements the basic RPC method logic without the actual -// underlying RPC call functionality, which is provided by `caller`. -type baseRPCClient struct { - caller http2.RPCCaller -} - -var ( - _ Client = (*HTTP)(nil) - _ Client = (*Batch)(nil) -) - -// ----------------------------------------------------------------------------- -// HTTP - -// NewHTTP takes a remote endpoint in the form ://: -// The function panics if the provided remote is invalid. -func NewHTTP(remote string) *HTTP { - httpClient := http2.DefaultHTTPClient(remote) - return NewHTTPWithClient(remote, httpClient) -} - -// NewHTTPWithClient allows for setting a custom http client. See NewHTTP -// The function panics if the provided client is nil or remote is invalid. -func NewHTTPWithClient(remote string, client *http.Client) *HTTP { - if client == nil { - panic("nil http.Client provided") - } - rc := http2.NewJSONRPCClientWithHTTPClient(remote, client) - - return &HTTP{ - rpc: rc, - remote: remote, - baseRPCClient: &baseRPCClient{caller: rc}, - } -} - -// NewBatch creates a new rpcBatch client for this HTTP client. -func (c *HTTP) NewBatch() *Batch { - batch := rpcclient.NewBatch(c.rpc) - return &Batch{ - rpcBatch: batch, - baseRPCClient: &baseRPCClient{ - caller: batch, - }, - } -} - -// Send is a convenience function for an HTTP rpcBatch 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 rpcBatch. -func (b *Batch) Send(ctx context.Context) ([]any, error) { - return b.rpcBatch.Send(ctx) -} - -// Clear will empty out this rpcBatch of requests and return the number of requests -// that were cleared out. -func (b *Batch) Clear() int { - return b.rpcBatch.Clear() -} - -// Count returns the number of enqueued requests waiting to be sent. -func (b *Batch) Count() int { - return b.rpcBatch.Count() -} diff --git a/tm2/pkg/bft/rpc/client/interface.go b/tm2/pkg/bft/rpc/client/interface.go deleted file mode 100644 index 0a13853b09b..00000000000 --- a/tm2/pkg/bft/rpc/client/interface.go +++ /dev/null @@ -1,95 +0,0 @@ -package client - -/* -The client package provides a general purpose interface (Client) for connecting -to a tendermint node, as well as higher-level functionality. - -The main implementation for production code is client.HTTP, which -connects via http to the jsonrpc interface of the tendermint node. - -For connecting to a node running in the same process (eg. when -compiling the abci app in the same process), you can use the client.Local -implementation. - -For mocking out server responses during testing to see behavior for -arbitrary return values, use the mock package. - -In addition to the Client interface, which should be used externally -for maximum flexibility and testability, and two implementations, -this package also provides helper functions that work on any Client -implementation. -*/ - -import ( - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" -) - -// Client wraps most important rpc calls a client would make. -// -// NOTE: Events cannot be subscribed to from the RPC APIs. For events -// subscriptions and filters and queries, an external API must be used that -// first synchronously consumes the events from the node's synchronous event -// switch, or reads logged events from the filesystem. -type Client interface { - ABCIClient - HistoryClient - NetworkClient - SignClient - StatusClient - MempoolClient -} - -// 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) - ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) - ABCIQueryWithOptions(path string, data []byte, - opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) - - // Writing to abci app - BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) - BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) - BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) -} - -// 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) - Commit(height *int64) (*ctypes.ResultCommit, error) - Validators(height *int64) (*ctypes.ResultValidators, error) -} - -// 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 { - Status() (*ctypes.ResultStatus, error) -} - -// NetworkClient is general info about the network state. May not be needed -// usually. -type NetworkClient interface { - NetInfo() (*ctypes.ResultNetInfo, error) - DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) - ConsensusState() (*ctypes.ResultConsensusState, error) - ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, error) - Health() (*ctypes.ResultHealth, error) -} - -// MempoolClient shows us data about current mempool state. -type MempoolClient interface { - UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) - NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) -} diff --git a/tm2/pkg/bft/rpc/client/local.go b/tm2/pkg/bft/rpc/client/local.go index cbfaccb35b2..c7ef599054c 100644 --- a/tm2/pkg/bft/rpc/client/local.go +++ b/tm2/pkg/bft/rpc/client/local.go @@ -137,13 +137,3 @@ func (c *Local) Commit(height *int64) (*ctypes.ResultCommit, error) { func (c *Local) Validators(height *int64) (*ctypes.ResultValidators, error) { return core.Validators(c.ctx, height) } - -/* -func (c *Local) Tx(hash []byte, prove bool) (*ctypes.ResultTx, error) { - return core.Tx(c.ctx, hash, prove) -} - -func (c *Local) TxSearch(query string, prove bool, page, perPage int) (*ctypes.ResultTxSearch, error) { - return core.TxSearch(c.ctx, query, prove, page, perPage) -} -*/ diff --git a/tm2/pkg/bft/rpc/client/main_test.go b/tm2/pkg/bft/rpc/client/main_test.go deleted file mode 100644 index 759104a3029..00000000000 --- a/tm2/pkg/bft/rpc/client/main_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package client_test - -import ( - "os" - "testing" - - "github.com/gnolang/gno/tm2/pkg/bft/abci/example/kvstore" - nm "github.com/gnolang/gno/tm2/pkg/bft/node" - rpctest "github.com/gnolang/gno/tm2/pkg/bft/rpc/test" -) - -var node *nm.Node - -func TestMain(m *testing.M) { - // start a tendermint node (and kvstore) in the background to test against - dir, err := os.MkdirTemp("/tmp", "rpc-client-test") - if err != nil { - panic(err) - } - app := kvstore.NewPersistentKVStoreApplication(dir) - node = rpctest.StartTendermint(app) - - code := m.Run() - - // and shut down proper at the end - rpctest.StopTendermint(node) - os.Exit(code) -} diff --git a/tm2/pkg/bft/rpc/client/mock/abci.go b/tm2/pkg/bft/rpc/client/mock/abci.go deleted file mode 100644 index af09fa6c43a..00000000000 --- a/tm2/pkg/bft/rpc/client/mock/abci.go +++ /dev/null @@ -1,209 +0,0 @@ -package mock - -import ( - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" -) - -// ABCIApp will send all abci related request to the named app, -// so you can test app behavior from a client without needing -// an entire tendermint node -type ABCIApp struct { - App abci.Application -} - -var ( - _ client.ABCIClient = ABCIApp{} - _ client.ABCIClient = ABCIMock{} - _ client.ABCIClient = (*ABCIRecorder)(nil) -) - -func (a ABCIApp) ABCIInfo() (*ctypes.ResultABCIInfo, error) { - return &ctypes.ResultABCIInfo{Response: a.App.Info(abci.RequestInfo{})}, nil -} - -func (a ABCIApp) ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) { - return a.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) -} - -func (a ABCIApp) ABCIQueryWithOptions(path string, data []byte, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { - q := a.App.Query(abci.RequestQuery{ - Data: data, - Path: path, - Height: opts.Height, - Prove: opts.Prove, - }) - return &ctypes.ResultABCIQuery{Response: q}, nil -} - -// NOTE: Caller should call a.App.Commit() separately, -// this function does not actually wait for a commit. -// 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(abci.RequestCheckTx{Tx: tx}) - if res.CheckTx.IsErr() { - return &res, nil - } - 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(abci.RequestCheckTx{Tx: tx}) - // and this gets written in a background thread... - if !c.IsErr() { - go func() { a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) }() //nolint: errcheck - } - return &ctypes.ResultBroadcastTx{Error: c.Error, Data: c.Data, Log: c.Log, Hash: tx.Hash()}, nil -} - -func (a ABCIApp) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - c := a.App.CheckTx(abci.RequestCheckTx{Tx: tx}) - // and this gets written in a background thread... - if !c.IsErr() { - go func() { a.App.DeliverTx(abci.RequestDeliverTx{Tx: tx}) }() //nolint: errcheck - } - return &ctypes.ResultBroadcastTx{Error: c.Error, Data: c.Data, Log: c.Log, Hash: tx.Hash()}, nil -} - -// ABCIMock will send all abci related request to the named app, -// so you can test app behavior from a client without needing -// an entire tendermint node -type ABCIMock struct { - Info Call - Query Call - BroadcastCommit Call - Broadcast Call -} - -func (m ABCIMock) ABCIInfo() (*ctypes.ResultABCIInfo, error) { - res, err := m.Info.GetResponse(nil) - if err != nil { - return nil, err - } - return &ctypes.ResultABCIInfo{Response: res.(abci.ResponseInfo)}, nil -} - -func (m ABCIMock) ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) { - return m.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) -} - -func (m ABCIMock) ABCIQueryWithOptions(path string, data []byte, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { - res, err := m.Query.GetResponse(QueryArgs{path, data, opts.Height, opts.Prove}) - if err != nil { - return nil, err - } - resQuery := res.(abci.ResponseQuery) - return &ctypes.ResultABCIQuery{Response: resQuery}, nil -} - -func (m ABCIMock) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - res, err := m.BroadcastCommit.GetResponse(tx) - if err != nil { - return nil, err - } - return res.(*ctypes.ResultBroadcastTxCommit), nil -} - -func (m ABCIMock) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - res, err := m.Broadcast.GetResponse(tx) - if err != nil { - return nil, err - } - return res.(*ctypes.ResultBroadcastTx), nil -} - -func (m ABCIMock) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - res, err := m.Broadcast.GetResponse(tx) - if err != nil { - return nil, err - } - return res.(*ctypes.ResultBroadcastTx), nil -} - -// ABCIRecorder can wrap another type (ABCIApp, ABCIMock, or Client) -// and record all ABCI related calls. -type ABCIRecorder struct { - Client client.ABCIClient - Calls []Call -} - -func NewABCIRecorder(client client.ABCIClient) *ABCIRecorder { - return &ABCIRecorder{ - Client: client, - Calls: []Call{}, - } -} - -type QueryArgs struct { - Path string - Data []byte - Height int64 - Prove bool -} - -func (r *ABCIRecorder) addCall(call Call) { - r.Calls = append(r.Calls, call) -} - -func (r *ABCIRecorder) ABCIInfo() (*ctypes.ResultABCIInfo, error) { - res, err := r.Client.ABCIInfo() - r.addCall(Call{ - Name: "abci_info", - Response: res, - Error: err, - }) - return res, err -} - -func (r *ABCIRecorder) ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) { - return r.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) -} - -func (r *ABCIRecorder) ABCIQueryWithOptions(path string, data []byte, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { - res, err := r.Client.ABCIQueryWithOptions(path, data, opts) - r.addCall(Call{ - Name: "abci_query", - Args: QueryArgs{path, data, opts.Height, opts.Prove}, - Response: res, - Error: err, - }) - return res, err -} - -func (r *ABCIRecorder) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - res, err := r.Client.BroadcastTxCommit(tx) - r.addCall(Call{ - Name: "broadcast_tx_commit", - Args: tx, - Response: res, - Error: err, - }) - return res, err -} - -func (r *ABCIRecorder) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - res, err := r.Client.BroadcastTxAsync(tx) - r.addCall(Call{ - Name: "broadcast_tx_async", - Args: tx, - Response: res, - Error: err, - }) - return res, err -} - -func (r *ABCIRecorder) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - res, err := r.Client.BroadcastTxSync(tx) - r.addCall(Call{ - Name: "broadcast_tx_sync", - Args: tx, - Response: res, - Error: err, - }) - return res, err -} diff --git a/tm2/pkg/bft/rpc/client/mock/abci_test.go b/tm2/pkg/bft/rpc/client/mock/abci_test.go deleted file mode 100644 index 08019807f33..00000000000 --- a/tm2/pkg/bft/rpc/client/mock/abci_test.go +++ /dev/null @@ -1,191 +0,0 @@ -package mock_test - -import ( - "fmt" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/tm2/pkg/bft/abci/example/kvstore" - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client/mock" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/errors" -) - -func TestABCIMock(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - key, value := []byte("foo"), []byte("bar") - height := int64(10) - goodTx := types.Tx{0x01, 0xff} - badTx := types.Tx{0x12, 0x21} - - m := mock.ABCIMock{ - Info: mock.Call{Error: errors.New("foobar")}, - Query: mock.Call{Response: abci.ResponseQuery{ - Key: key, - Value: value, - Height: height, - }}, - // Broadcast commit depends on call - BroadcastCommit: mock.Call{ - Args: goodTx, - Response: &ctypes.ResultBroadcastTxCommit{ - CheckTx: abci.ResponseCheckTx{ResponseBase: abci.ResponseBase{Data: []byte("stand")}}, - DeliverTx: abci.ResponseDeliverTx{ResponseBase: abci.ResponseBase{Data: []byte("deliver")}}, - }, - Error: errors.New("bad tx"), - }, - Broadcast: mock.Call{Error: errors.New("must commit")}, - } - - // now, let's try to make some calls - _, err := m.ABCIInfo() - require.NotNil(err) - assert.Equal("foobar", err.Error()) - - // query always returns the response - _query, err := m.ABCIQueryWithOptions("/", nil, client.ABCIQueryOptions{Prove: false}) - query := _query.Response - require.Nil(err) - require.NotNil(query) - assert.EqualValues(key, query.Key) - assert.EqualValues(value, query.Value) - assert.Equal(height, query.Height) - - // non-commit calls always return errors - _, err = m.BroadcastTxSync(goodTx) - require.NotNil(err) - assert.Equal("must commit", err.Error()) - _, err = m.BroadcastTxAsync(goodTx) - require.NotNil(err) - assert.Equal("must commit", err.Error()) - - // commit depends on the input - _, err = m.BroadcastTxCommit(badTx) - require.NotNil(err) - assert.Equal("bad tx", err.Error()) - bres, err := m.BroadcastTxCommit(goodTx) - require.Nil(err, "%+v", err) - assert.Nil(bres.CheckTx.Error) - assert.EqualValues("stand", string(bres.CheckTx.Data)) - assert.EqualValues("deliver", string(bres.DeliverTx.Data)) -} - -func TestABCIRecorder(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - // This mock returns errors on everything but Query - m := mock.ABCIMock{ - Info: mock.Call{Response: abci.ResponseInfo{ - ResponseBase: abci.ResponseBase{ - Data: []byte("data"), - }, - ABCIVersion: "v0.0.0test", - AppVersion: "v0.0.0test", - }}, - Query: mock.Call{Error: errors.New("query")}, - Broadcast: mock.Call{Error: errors.New("broadcast")}, - BroadcastCommit: mock.Call{Error: errors.New("broadcast_commit")}, - } - r := mock.NewABCIRecorder(m) - - require.Equal(0, len(r.Calls)) - - _, err := r.ABCIInfo() - assert.Nil(err, "expected no err on info") - - _, err = r.ABCIQueryWithOptions("path", []byte("data"), client.ABCIQueryOptions{Prove: false}) - assert.NotNil(err, "expected error on query") - require.Equal(2, len(r.Calls)) - - info := r.Calls[0] - assert.Equal("abci_info", info.Name) - assert.Nil(info.Error) - assert.Nil(info.Args) - require.NotNil(info.Response) - ir, ok := info.Response.(*ctypes.ResultABCIInfo) - require.True(ok) - assert.Equal("data", string(ir.Response.Data)) - assert.Equal("v0.0.0test", ir.Response.ABCIVersion) - assert.Equal("v0.0.0test", ir.Response.AppVersion) - - query := r.Calls[1] - assert.Equal("abci_query", query.Name) - assert.Nil(query.Response) - require.NotNil(query.Error) - assert.Equal("query", query.Error.Error()) - require.NotNil(query.Args) - qa, ok := query.Args.(mock.QueryArgs) - require.True(ok) - assert.Equal("path", qa.Path) - assert.EqualValues("data", string(qa.Data)) - assert.False(qa.Prove) - - // now add some broadcasts (should all err) - txs := []types.Tx{{1}, {2}, {3}} - _, err = r.BroadcastTxCommit(txs[0]) - assert.NotNil(err, "expected err on broadcast") - _, err = r.BroadcastTxSync(txs[1]) - assert.NotNil(err, "expected err on broadcast") - _, err = r.BroadcastTxAsync(txs[2]) - assert.NotNil(err, "expected err on broadcast") - - require.Equal(5, len(r.Calls)) - - bc := r.Calls[2] - assert.Equal("broadcast_tx_commit", bc.Name) - assert.Nil(bc.Response) - require.NotNil(bc.Error) - assert.EqualValues(bc.Args, txs[0]) - - bs := r.Calls[3] - assert.Equal("broadcast_tx_sync", bs.Name) - assert.Nil(bs.Response) - require.NotNil(bs.Error) - assert.EqualValues(bs.Args, txs[1]) - - ba := r.Calls[4] - assert.Equal("broadcast_tx_async", ba.Name) - assert.Nil(ba.Response) - require.NotNil(ba.Error) - assert.EqualValues(ba.Args, txs[2]) -} - -func TestABCIApp(t *testing.T) { - assert, require := assert.New(t), require.New(t) - app := kvstore.NewKVStoreApplication() - m := mock.ABCIApp{app} - - // get some info - info, err := m.ABCIInfo() - require.Nil(err) - assert.Equal(`{"size":0}`, string(info.Response.Data)) - - // add a key - key, value := "foo", "bar" - tx := fmt.Sprintf("%s=%s", key, value) - res, err := m.BroadcastTxCommit(types.Tx(tx)) - require.Nil(err) - assert.True(res.CheckTx.IsOK()) - require.NotNil(res.DeliverTx) - assert.True(res.DeliverTx.IsOK()) - - // commit - // TODO: This may not be necessary in the future - if res.Height == -1 { - m.App.Commit() - } - - // check the key - _qres, err := m.ABCIQueryWithOptions("/key", []byte(key), client.ABCIQueryOptions{Prove: true}) - qres := _qres.Response - require.Nil(err) - assert.EqualValues(value, qres.Value) - - // XXX Check proof -} diff --git a/tm2/pkg/bft/rpc/client/mock/client.go b/tm2/pkg/bft/rpc/client/mock/client.go deleted file mode 100644 index dfb629daaec..00000000000 --- a/tm2/pkg/bft/rpc/client/mock/client.go +++ /dev/null @@ -1,152 +0,0 @@ -package mock - -/* -package mock returns a Client implementation that -accepts various (mock) implementations of the various methods. - -This implementation is useful for using in tests, when you don't -need a real server, but want a high-level of control about -the server response you want to mock (eg. error handling), -or if you just want to record the calls to verify in your tests. - -For real clients, you probably want the "http" package. If you -want to directly call a tendermint node in process, you can use the -"local" package. -*/ - -import ( - "reflect" - - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/core" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/bft/types" - "github.com/gnolang/gno/tm2/pkg/service" -) - -// Client wraps arbitrary implementations of the various interfaces. -// -// We provide a few choices to mock out each one in this package. -// Nothing hidden here, so no New function, just construct it from -// some parts, and swap them out them during the tests. -type Client struct { - client.ABCIClient - client.SignClient - client.HistoryClient - client.StatusClient - client.MempoolClient - service.Service -} - -var _ client.Client = Client{} - -// Call is used by recorders to save a call and response. -// It can also be used to configure mock responses. -type Call struct { - Name string - Args any - Response any - Error error -} - -// GetResponse will generate the appropriate response for us, when -// using the Call struct to configure a Mock handler. -// -// When configuring a response, if only one of Response or Error is -// set then that will always be returned. If both are set, then -// we return Response if the Args match the set args, Error otherwise. -func (c Call) GetResponse(args any) (any, error) { - // handle the case with no response - if c.Response == nil { - if c.Error == nil { - panic("Misconfigured call, you must set either Response or Error") - } - return nil, c.Error - } - // response without error - if c.Error == nil { - return c.Response, nil - } - // have both, we must check args.... - if reflect.DeepEqual(args, c.Args) { - return c.Response, nil - } - return nil, c.Error -} - -func (c Client) Status() (*ctypes.ResultStatus, error) { - return core.Status(&rpctypes.Context{}) -} - -func (c Client) ABCIInfo() (*ctypes.ResultABCIInfo, error) { - return core.ABCIInfo(&rpctypes.Context{}) -} - -func (c Client) ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) { - return c.ABCIQueryWithOptions(path, data, client.DefaultABCIQueryOptions) -} - -func (c Client) ABCIQueryWithOptions(path string, data []byte, opts client.ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) { - return core.ABCIQuery(&rpctypes.Context{}, path, data, opts.Height, opts.Prove) -} - -func (c Client) BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) { - return core.BroadcastTxCommit(&rpctypes.Context{}, tx) -} - -func (c Client) BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - return core.BroadcastTxAsync(&rpctypes.Context{}, tx) -} - -func (c Client) BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) { - return core.BroadcastTxSync(&rpctypes.Context{}, tx) -} - -func (c Client) NetInfo() (*ctypes.ResultNetInfo, error) { - return core.NetInfo(&rpctypes.Context{}) -} - -func (c Client) ConsensusState() (*ctypes.ResultConsensusState, error) { - return core.ConsensusState(&rpctypes.Context{}) -} - -func (c Client) ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, error) { - return core.ConsensusParams(&rpctypes.Context{}, height) -} - -func (c Client) DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) { - return core.DumpConsensusState(&rpctypes.Context{}) -} - -func (c Client) Health() (*ctypes.ResultHealth, error) { - return core.Health(&rpctypes.Context{}) -} - -func (c Client) DialSeeds(seeds []string) (*ctypes.ResultDialSeeds, error) { - return core.UnsafeDialSeeds(&rpctypes.Context{}, seeds) -} - -func (c Client) DialPeers(peers []string, persistent bool) (*ctypes.ResultDialPeers, error) { - return core.UnsafeDialPeers(&rpctypes.Context{}, peers, persistent) -} - -func (c Client) BlockchainInfo(minHeight, maxHeight int64) (*ctypes.ResultBlockchainInfo, error) { - return core.BlockchainInfo(&rpctypes.Context{}, minHeight, maxHeight) -} - -func (c Client) Genesis() (*ctypes.ResultGenesis, error) { - return core.Genesis(&rpctypes.Context{}) -} - -func (c Client) Block(height *int64) (*ctypes.ResultBlock, error) { - return core.Block(&rpctypes.Context{}, height) -} - -func (c Client) Commit(height *int64) (*ctypes.ResultCommit, error) { - return core.Commit(&rpctypes.Context{}, height) -} - -func (c Client) Validators(height *int64) (*ctypes.ResultValidators, error) { - return core.Validators(&rpctypes.Context{}, height) -} diff --git a/tm2/pkg/bft/rpc/client/mock/status.go b/tm2/pkg/bft/rpc/client/mock/status.go deleted file mode 100644 index e5a1d84209b..00000000000 --- a/tm2/pkg/bft/rpc/client/mock/status.go +++ /dev/null @@ -1,52 +0,0 @@ -package mock - -import ( - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" -) - -// StatusMock returns the result specified by the Call -type StatusMock struct { - Call -} - -var ( - _ client.StatusClient = (*StatusMock)(nil) - _ client.StatusClient = (*StatusRecorder)(nil) -) - -func (m *StatusMock) Status() (*ctypes.ResultStatus, error) { - res, err := m.GetResponse(nil) - if err != nil { - return nil, err - } - return res.(*ctypes.ResultStatus), nil -} - -// StatusRecorder can wrap another type (StatusMock, full client) -// and record the status calls -type StatusRecorder struct { - Client client.StatusClient - Calls []Call -} - -func NewStatusRecorder(client client.StatusClient) *StatusRecorder { - return &StatusRecorder{ - Client: client, - Calls: []Call{}, - } -} - -func (r *StatusRecorder) addCall(call Call) { - r.Calls = append(r.Calls, call) -} - -func (r *StatusRecorder) Status() (*ctypes.ResultStatus, error) { - res, err := r.Client.Status() - r.addCall(Call{ - Name: "status", - Response: res, - Error: err, - }) - return res, err -} diff --git a/tm2/pkg/bft/rpc/client/mock/status_test.go b/tm2/pkg/bft/rpc/client/mock/status_test.go deleted file mode 100644 index ad2f998eed7..00000000000 --- a/tm2/pkg/bft/rpc/client/mock/status_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package mock_test - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client/mock" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" -) - -func TestStatus(t *testing.T) { - assert, require := assert.New(t), require.New(t) - - m := &mock.StatusMock{ - Call: mock.Call{ - Response: &ctypes.ResultStatus{ - SyncInfo: ctypes.SyncInfo{ - LatestBlockHash: []byte("block"), - LatestAppHash: []byte("app"), - LatestBlockHeight: 10, - }, - }, - }, - } - - r := mock.NewStatusRecorder(m) - require.Equal(0, len(r.Calls)) - - // make sure response works proper - status, err := r.Status() - require.Nil(err, "%+v", err) - assert.EqualValues("block", status.SyncInfo.LatestBlockHash) - assert.EqualValues(10, status.SyncInfo.LatestBlockHeight) - - // make sure recorder works properly - require.Equal(1, len(r.Calls)) - rs := r.Calls[0] - assert.Equal("status", rs.Name) - assert.Nil(rs.Args) - assert.Nil(rs.Error) - require.NotNil(rs.Response) - st, ok := rs.Response.(*ctypes.ResultStatus) - require.True(ok) - assert.EqualValues("block", st.SyncInfo.LatestBlockHash) - assert.EqualValues(10, st.SyncInfo.LatestBlockHeight) -} diff --git a/tm2/pkg/bft/rpc/client/mock_test.go b/tm2/pkg/bft/rpc/client/mock_test.go new file mode 100644 index 00000000000..bc751b4bed0 --- /dev/null +++ b/tm2/pkg/bft/rpc/client/mock_test.go @@ -0,0 +1,33 @@ +package client + +import ( + "context" + + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" +) + +type ( + sendRequestDelegate func(context.Context, types.RPCRequest) (*types.RPCResponse, error) + sendBatchDelegate func(context.Context, types.RPCRequests) (types.RPCResponses, error) +) + +type mockClient struct { + sendRequestFn sendRequestDelegate + sendBatchFn sendBatchDelegate +} + +func (m *mockClient) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) { + if m.sendRequestFn != nil { + return m.sendRequestFn(ctx, request) + } + + return nil, nil +} + +func (m *mockClient) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) { + if m.sendBatchFn != nil { + return m.sendBatchFn(ctx, requests) + } + + return nil, nil +} diff --git a/tm2/pkg/bft/rpc/client/options.go b/tm2/pkg/bft/rpc/client/options.go new file mode 100644 index 00000000000..e4b0a1a89d2 --- /dev/null +++ b/tm2/pkg/bft/rpc/client/options.go @@ -0,0 +1,12 @@ +package client + +import "time" + +type Option func(client *RPCClient) + +// WithRequestTimeout sets the request timeout +func WithRequestTimeout(timeout time.Duration) Option { + return func(client *RPCClient) { + client.requestTimeout = timeout + } +} diff --git a/tm2/pkg/bft/rpc/client/rpc_test.go b/tm2/pkg/bft/rpc/client/rpc_test.go deleted file mode 100644 index c1bc7779f36..00000000000 --- a/tm2/pkg/bft/rpc/client/rpc_test.go +++ /dev/null @@ -1,597 +0,0 @@ -package client_test - -import ( - "context" - "net/http" - "strings" - "sync" - "testing" - - rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/gnolang/gno/tm2/pkg/bft/rpc/client" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - rpctest "github.com/gnolang/gno/tm2/pkg/bft/rpc/test" - "github.com/gnolang/gno/tm2/pkg/bft/types" -) - -func getHTTPClient() *client.HTTP { - rpcAddr := rpctest.GetConfig().RPC.ListenAddress - return client.NewHTTP(rpcAddr) -} - -func getLocalClient() *client.Local { - return client.NewLocal() -} - -// GetClients returns a slice of clients for table-driven tests -func GetClients() []client.Client { - return []client.Client{ - getHTTPClient(), - getLocalClient(), - } -} - -func TestNilCustomHTTPClient(t *testing.T) { - t.Parallel() - - require.Panics(t, func() { - client.NewHTTPWithClient("http://example.com", nil) - }) - require.Panics(t, func() { - rpcclient.NewJSONRPCClientWithHTTPClient("http://example.com", nil) - }) -} - -func TestCustomHTTPClient(t *testing.T) { - t.Parallel() - - remote := rpctest.GetConfig().RPC.ListenAddress - c := client.NewHTTPWithClient(remote, http.DefaultClient) - status, err := c.Status() - require.NoError(t, err) - require.NotNil(t, status) -} - -func TestCorsEnabled(t *testing.T) { - t.Parallel() - - origin := rpctest.GetConfig().RPC.CORSAllowedOrigins[0] - remote := strings.Replace(rpctest.GetConfig().RPC.ListenAddress, "tcp", "http", -1) - - req, err := http.NewRequest("GET", remote, nil) - require.Nil(t, err, "%+v", err) - req.Header.Set("Origin", origin) - c := &http.Client{} - resp, err := c.Do(req) - require.Nil(t, err, "%+v", err) - defer resp.Body.Close() - - assert.Equal(t, resp.Header.Get("Access-Control-Allow-Origin"), origin) -} - -// Make sure status is correct (we connect properly) -func TestStatus(t *testing.T) { - t.Parallel() - - for i, c := range GetClients() { - moniker := rpctest.GetConfig().Moniker - status, err := c.Status() - require.Nil(t, err, "%d: %+v", i, err) - assert.Equal(t, moniker, status.NodeInfo.Moniker) - } -} - -// Make sure info is correct (we connect properly) -func TestInfo(t *testing.T) { - t.Parallel() - - for i, c := range GetClients() { - // status, err := c.Status() - // require.Nil(t, err, "%+v", err) - info, err := c.ABCIInfo() - require.Nil(t, err, "%d: %+v", i, err) - // TODO: this is not correct - fix merkleeyes! - // assert.EqualValues(t, status.SyncInfo.LatestBlockHeight, info.Response.LastBlockHeight) - assert.True(t, strings.Contains(string(info.Response.ResponseBase.Data), "size")) - } -} - -func TestNetInfo(t *testing.T) { - t.Parallel() - - for i, c := range GetClients() { - nc, ok := c.(client.NetworkClient) - require.True(t, ok, "%d", i) - netinfo, err := nc.NetInfo() - require.Nil(t, err, "%d: %+v", i, err) - assert.True(t, netinfo.Listening) - assert.Equal(t, 0, len(netinfo.Peers)) - } -} - -func TestDumpConsensusState(t *testing.T) { - t.Parallel() - - for i, c := range GetClients() { - // FIXME: fix server so it doesn't panic on invalid input - nc, ok := c.(client.NetworkClient) - require.True(t, ok, "%d", i) - cons, err := nc.DumpConsensusState() - require.Nil(t, err, "%d: %+v", i, err) - assert.NotEmpty(t, cons.RoundState) - assert.Empty(t, cons.Peers) - } -} - -func TestConsensusState(t *testing.T) { - t.Parallel() - - for i, c := range GetClients() { - // FIXME: fix server so it doesn't panic on invalid input - nc, ok := c.(client.NetworkClient) - require.True(t, ok, "%d", i) - cons, err := nc.ConsensusState() - require.Nil(t, err, "%d: %+v", i, err) - assert.NotEmpty(t, cons.RoundState) - } -} - -func TestHealth(t *testing.T) { - t.Parallel() - - for i, c := range GetClients() { - nc, ok := c.(client.NetworkClient) - require.True(t, ok, "%d", i) - _, err := nc.Health() - require.Nil(t, err, "%d: %+v", i, err) - } -} - -func TestGenesisAndValidators(t *testing.T) { - t.Parallel() - - for i, c := range GetClients() { - // make sure this is the right genesis file - gen, err := c.Genesis() - require.Nil(t, err, "%d: %+v", i, err) - // get the genesis validator - require.Equal(t, 1, len(gen.Genesis.Validators)) - gval := gen.Genesis.Validators[0] - - // get the current validators - vals, err := c.Validators(nil) - require.Nil(t, err, "%d: %+v", i, err) - require.Equal(t, 1, len(vals.Validators)) - val := vals.Validators[0] - - // make sure the current set is also the genesis set - assert.Equal(t, gval.Power, val.VotingPower) - assert.Equal(t, gval.PubKey, val.PubKey) - } -} - -func TestABCIQuery(t *testing.T) { - for i, c := range GetClients() { - // write something - k, v, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(t, err, "%d: %+v", i, err) - apph := bres.Height + 1 // this is where the tx will be applied to the state - - // wait before querying - client.WaitForHeight(c, apph, nil) - res, err := c.ABCIQuery("/key", k) - qres := res.Response - if assert.Nil(t, err) && assert.True(t, qres.IsOK()) { - assert.EqualValues(t, v, qres.Value) - } - } -} - -// Make some app checks -func TestAppCalls(t *testing.T) { - t.Parallel() - - assert, require := assert.New(t), require.New(t) - for i, c := range GetClients() { - // get an offset of height to avoid racing and guessing - s, err := c.Status() - require.Nil(err, "%d: %+v", i, err) - // sh is start height or status height - sh := s.SyncInfo.LatestBlockHeight - - // look for the future - h := sh + 2 - _, err = c.Block(&h) - assert.NotNil(err) // no block yet - - // write something - k, v, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(err, "%d: %+v", i, err) - require.True(bres.DeliverTx.IsOK()) - txh := bres.Height - apph := txh + 1 // this is where the tx will be applied to the state - - // wait before querying - if err := client.WaitForHeight(c, apph, nil); err != nil { - t.Error(err) - } - _qres, err := c.ABCIQueryWithOptions("/key", k, client.ABCIQueryOptions{Prove: false}) - qres := _qres.Response - if assert.Nil(err) && assert.True(qres.IsOK()) { - assert.Equal(k, qres.Key) - assert.EqualValues(v, qres.Value) - } - - /* - // make sure we can lookup the tx with proof - ptx, err := c.Tx(bres.Hash, true) - require.Nil(err, "%d: %+v", i, err) - assert.EqualValues(txh, ptx.Height) - assert.EqualValues(tx, ptx.Tx) - */ - - // and we can even check the block is added - block, err := c.Block(&apph) - require.Nil(err, "%d: %+v", i, err) - appHash := block.BlockMeta.Header.AppHash - assert.True(len(appHash) > 0) - assert.EqualValues(apph, block.BlockMeta.Header.Height) - - // now check the results - blockResults, err := c.BlockResults(&txh) - require.Nil(err, "%d: %+v", i, err) - assert.Equal(txh, blockResults.Height) - if assert.Equal(1, len(blockResults.Results.DeliverTxs)) { - // check success code - assert.Nil(blockResults.Results.DeliverTxs[0].Error) - } - - // check blockchain info, now that we know there is info - info, err := c.BlockchainInfo(apph, apph) - require.Nil(err, "%d: %+v", i, err) - assert.True(info.LastHeight >= apph) - if assert.Equal(1, len(info.BlockMetas)) { - lastMeta := info.BlockMetas[0] - assert.EqualValues(apph, lastMeta.Header.Height) - bMeta := block.BlockMeta - assert.Equal(bMeta.Header.AppHash, lastMeta.Header.AppHash) - assert.Equal(bMeta.BlockID, lastMeta.BlockID) - } - - // and get the corresponding commit with the same apphash - commit, err := c.Commit(&apph) - require.Nil(err, "%d: %+v", i, err) - cappHash := commit.Header.AppHash - assert.Equal(appHash, cappHash) - assert.NotNil(commit.Commit) - - // compare the commits (note Commit(2) has commit from Block(3)) - h = apph - 1 - commit2, err := c.Commit(&h) - require.Nil(err, "%d: %+v", i, err) - assert.Equal(block.Block.LastCommit, commit2.Commit) - - // and we got a proof that works! - _pres, err := c.ABCIQueryWithOptions("/key", k, client.ABCIQueryOptions{Prove: true}) - pres := _pres.Response - assert.Nil(err) - assert.True(pres.IsOK()) - - // XXX Test proof - } -} - -func TestBroadcastTxSync(t *testing.T) { - t.Parallel() - - require := require.New(t) - - // 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() { - _, _, tx := MakeTxKV() - bres, err := c.BroadcastTxSync(tx) - require.Nil(err, "%d: %+v", i, err) - require.Nil(bres.Error) - - require.Equal(initMempoolSize+1, mempool.Size()) - - txs := mempool.ReapMaxTxs(len(tx)) - require.EqualValues(tx, txs[0]) - mempool.Flush() - } -} - -func TestBroadcastTxCommit(t *testing.T) { - require := require.New(t) - - mempool := node.Mempool() - for i, c := range GetClients() { - _, _, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(err, "%d: %+v", i, err) - require.True(bres.CheckTx.IsOK()) - require.True(bres.DeliverTx.IsOK()) - - require.Equal(0, mempool.Size()) - } -} - -func TestUnconfirmedTxs(t *testing.T) { - _, _, tx := MakeTxKV() - - mempool := node.Mempool() - _ = mempool.CheckTx(tx, nil) - - for i, c := range GetClients() { - mc, ok := c.(client.MempoolClient) - require.True(t, ok, "%d", i) - res, err := mc.UnconfirmedTxs(1) - require.Nil(t, err, "%d: %+v", i, err) - - assert.Equal(t, 1, res.Count) - assert.Equal(t, 1, res.Total) - assert.Equal(t, mempool.TxsBytes(), res.TotalBytes) - assert.Exactly(t, types.Txs{tx}, types.Txs(res.Txs)) - } - - mempool.Flush() -} - -func TestNumUnconfirmedTxs(t *testing.T) { - _, _, tx := MakeTxKV() - - mempool := node.Mempool() - _ = mempool.CheckTx(tx, nil) - mempoolSize := mempool.Size() - - for i, c := range GetClients() { - mc, ok := c.(client.MempoolClient) - require.True(t, ok, "%d", i) - res, err := mc.NumUnconfirmedTxs() - require.Nil(t, err, "%d: %+v", i, err) - - assert.Equal(t, mempoolSize, res.Count) - assert.Equal(t, mempoolSize, res.Total) - assert.Equal(t, mempool.TxsBytes(), res.TotalBytes) - } - - mempool.Flush() -} - -/* -func TestTx(t *testing.T) { - t.Parallel() - - // first we broadcast a tx - c := getHTTPClient() - _, _, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(t, err, "%+v", err) - - txHeight := bres.Height - txHash := bres.Hash - - anotherTxHash := types.Tx("a different tx").Hash() - - cases := []struct { - valid bool - hash []byte - prove bool - }{ - // only valid if correct hash provided - {true, txHash, false}, - {true, txHash, true}, - {false, anotherTxHash, false}, - {false, anotherTxHash, true}, - {false, nil, false}, - {false, nil, true}, - } - - for i, c := range GetClients() { - for j, tc := range cases { - t.Logf("client %d, case %d", i, j) - - // now we query for the tx. - // since there's only one tx, we know index=0. - ptx, err := c.Tx(tc.hash, tc.prove) - - if !tc.valid { - require.NotNil(t, err) - } else { - require.Nil(t, err, "%+v", err) - assert.EqualValues(t, txHeight, ptx.Height) - assert.EqualValues(t, tx, ptx.Tx) - assert.Zero(t, ptx.Index) - assert.True(t, ptx.TxResult.IsOK()) - assert.EqualValues(t, txHash, ptx.Hash) - - // time to verify the proof - proof := ptx.Proof - if tc.prove && assert.EqualValues(t, tx, proof.Data) { - assert.NoError(t, proof.Proof.Verify(proof.RootHash, txHash)) - } - } - } - } -} - -func TestTxSearch(t *testing.T) { - t.Parallel() - - // first we broadcast a tx - c := getHTTPClient() - _, _, tx := MakeTxKV() - bres, err := c.BroadcastTxCommit(tx) - require.Nil(t, err, "%+v", err) - - txHeight := bres.Height - txHash := bres.Hash - - anotherTxHash := types.Tx("a different tx").Hash() - - for i, c := range GetClients() { - t.Logf("client %d", i) - - // now we query for the tx. - // since there's only one tx, we know index=0. - result, err := c.TxSearch(fmt.Sprintf("tx.hash='%v'", txHash), true, 1, 30) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 1) - - ptx := result.Txs[0] - assert.EqualValues(t, txHeight, ptx.Height) - assert.EqualValues(t, tx, ptx.Tx) - assert.Zero(t, ptx.Index) - assert.True(t, ptx.TxResult.IsOK()) - assert.EqualValues(t, txHash, ptx.Hash) - - // time to verify the proof - proof := ptx.Proof - if assert.EqualValues(t, tx, proof.Data) { - assert.NoError(t, proof.Proof.Verify(proof.RootHash, txHash)) - } - - // query by height - result, err = c.TxSearch(fmt.Sprintf("tx.height=%d", txHeight), true, 1, 30) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 1) - - // query for non existing tx - result, err = c.TxSearch(fmt.Sprintf("tx.hash='%X'", anotherTxHash), false, 1, 30) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 0) - - // query using a tag (see kvstore application) - result, err = c.TxSearch("app.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) - 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) - require.Nil(t, err, "%+v", err) - require.Len(t, result.Txs, 0) - } -} -*/ - -func TestBatchedJSONRPCCalls(t *testing.T) { - c := getHTTPClient() - testBatchedJSONRPCCalls(t, c) -} - -func testBatchedJSONRPCCalls(t *testing.T, c *client.HTTP) { - t.Helper() - - 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(context.Background()) - 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 := max(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(context.Background()) - 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) { - t.Parallel() - - 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) { - t.Parallel() - - c := getHTTPClient() - batch := c.NewBatch() - _, err := batch.Send(context.Background()) - require.Error(t, err, "sending an empty rpcBatch of JSON RPC requests should result in an error") -} - -func TestClearingEmptyJSONRPCRequestBatch(t *testing.T) { - t.Parallel() - - c := getHTTPClient() - batch := c.NewBatch() - require.Zero(t, batch.Clear(), "clearing an empty rpcBatch 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/tm2/pkg/bft/rpc/client/types.go b/tm2/pkg/bft/rpc/client/types.go index 6a23fa4509d..3ac066f423e 100644 --- a/tm2/pkg/bft/rpc/client/types.go +++ b/tm2/pkg/bft/rpc/client/types.go @@ -1,5 +1,10 @@ package client +import ( + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" +) + // ABCIQueryOptions can be used to provide options for ABCIQuery call other // than the DefaultABCIQueryOptions. type ABCIQueryOptions struct { @@ -9,3 +14,72 @@ type ABCIQueryOptions struct { // DefaultABCIQueryOptions are latest height (0) and prove false. var DefaultABCIQueryOptions = ABCIQueryOptions{Height: 0, Prove: false} + +// Client wraps most important rpc calls a client would make. +// +// NOTE: Events cannot be subscribed to from the RPC APIs. For events +// subscriptions and filters and queries, an external API must be used that +// first synchronously consumes the events from the node's synchronous event +// switch, or reads logged events from the filesystem. +type Client interface { + ABCIClient + HistoryClient + NetworkClient + SignClient + StatusClient + MempoolClient +} + +// 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) + ABCIQuery(path string, data []byte) (*ctypes.ResultABCIQuery, error) + ABCIQueryWithOptions(path string, data []byte, + opts ABCIQueryOptions) (*ctypes.ResultABCIQuery, error) + + // Writing to abci app + BroadcastTxCommit(tx types.Tx) (*ctypes.ResultBroadcastTxCommit, error) + BroadcastTxAsync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) + BroadcastTxSync(tx types.Tx) (*ctypes.ResultBroadcastTx, error) +} + +// 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) + Commit(height *int64) (*ctypes.ResultCommit, error) + Validators(height *int64) (*ctypes.ResultValidators, error) +} + +// 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 { + Status() (*ctypes.ResultStatus, error) +} + +// NetworkClient is general info about the network state. May not be needed +// usually. +type NetworkClient interface { + NetInfo() (*ctypes.ResultNetInfo, error) + DumpConsensusState() (*ctypes.ResultDumpConsensusState, error) + ConsensusState() (*ctypes.ResultConsensusState, error) + ConsensusParams(height *int64) (*ctypes.ResultConsensusParams, error) + Health() (*ctypes.ResultHealth, error) +} + +// MempoolClient shows us data about current mempool state. +type MempoolClient interface { + UnconfirmedTxs(limit int) (*ctypes.ResultUnconfirmedTxs, error) + NumUnconfirmedTxs() (*ctypes.ResultUnconfirmedTxs, error) +} diff --git a/tm2/pkg/bft/rpc/client/ws.go b/tm2/pkg/bft/rpc/client/ws.go deleted file mode 100644 index 300c69d8b27..00000000000 --- a/tm2/pkg/bft/rpc/client/ws.go +++ /dev/null @@ -1,31 +0,0 @@ -package client - -import ( - "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/ws" -) - -var _ Client = (*WS)(nil) - -type WS struct { - rpc *ws.Client - - *baseRPCClient -} - -// func NewWS(remote, endpoint string) *WS { -// return &WS{ -// rpc: ws.NewClient(remote, endpoint), -// } -// } -// -// // NewBatch creates a new rpcBatch client for this HTTP client. -// func (c *WS) NewBatch() *Batch { -// batch := rpcclient.NewRPCRequestBatch(c.rpc) -// -// return &Batch{ -// rpcBatch: batch, -// baseRPCClient: &baseRPCClient{ -// caller: batch, -// }, -// } -// } diff --git a/tm2/pkg/bft/rpc/lib/client/http/client.go b/tm2/pkg/bft/rpc/lib/client/http/client.go index cbcb49f4069..4e6e760c180 100644 --- a/tm2/pkg/bft/rpc/lib/client/http/client.go +++ b/tm2/pkg/bft/rpc/lib/client/http/client.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -21,6 +22,11 @@ const ( protoTCP = "tcp" ) +var ( + ErrRequestResponseIDMismatch = errors.New("http request / response ID mismatch") + ErrInvalidBatchResponse = errors.New("invalid http batch response size") +) + // Client is an HTTP client implementation type Client struct { rpcURL string // the remote RPC URL of the node @@ -46,12 +52,41 @@ func NewClient(rpcURL string) (*Client, error) { // SendRequest sends a single RPC request to the server func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) { - return sendRequestCommon[types.RPCRequest, *types.RPCResponse](ctx, c.client, c.rpcURL, request) + // Send the request + response, err := sendRequestCommon[types.RPCRequest, *types.RPCResponse](ctx, c.client, c.rpcURL, request) + if err != nil { + return nil, err + } + + // Make sure the ID matches + if response.ID != response.ID { + return nil, ErrRequestResponseIDMismatch + } + + return response, nil } // SendBatch sends a single RPC batch request to the server func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) { - return sendRequestCommon[types.RPCRequests, types.RPCResponses](ctx, c.client, c.rpcURL, requests) + // Send the batch + responses, err := sendRequestCommon[types.RPCRequests, types.RPCResponses](ctx, c.client, c.rpcURL, requests) + if err != nil { + return nil, err + } + + // Make sure the length matches + if len(responses) != len(requests) { + return nil, ErrInvalidBatchResponse + } + + // Make sure the IDs match + for index, response := range responses { + if requests[index].ID != response.ID { + return nil, ErrRequestResponseIDMismatch + } + } + + return responses, nil } type ( diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client.go b/tm2/pkg/bft/rpc/lib/client/ws/client.go index 0d8b6ee6249..1254ca719f3 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client.go @@ -14,7 +14,11 @@ import ( "github.com/gorilla/websocket" ) -var errTimedOut = errors.New("context timed out") +var ( + ErrTimedOut = errors.New("context timed out") + ErrRequestResponseIDMismatch = errors.New("ws request / response ID mismatch") + ErrInvalidBatchResponse = errors.New("invalid ws batch response size") +) type responseCh chan<- types.RPCResponses @@ -62,19 +66,13 @@ func NewClient(rpcURL string, opts ...Option) (*Client, error) { return c, nil } -// SendBatch sends a batch of RPC requests to the server -func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) { +// SendRequest sends a single RPC request to the server +func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) { // Create the response channel for the pipeline responseCh := make(chan types.RPCResponses, 1) // Generate a unique request ID hash - requestIDs := make([]string, 0, len(requests)) - - for _, request := range requests { - requestIDs = append(requestIDs, request.ID.String()) - } - - requestHash := generateIDHash(requestIDs...) + requestHash := generateIDHash(request.ID.String()) c.requestMapMux.Lock() c.requestMap[requestHash] = responseCh @@ -83,26 +81,37 @@ func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (typ // Pipe the request to the backlog select { case <-ctx.Done(): - return nil, errTimedOut - case c.backlog <- requests: + return nil, ErrTimedOut + case c.backlog <- request: } // Wait for the response select { case <-ctx.Done(): - return nil, errTimedOut - case responses := <-responseCh: - return responses, nil + return nil, ErrTimedOut + case response := <-responseCh: + // Make sure the ID matches + if response[0].ID != request.ID { + return nil, ErrRequestResponseIDMismatch + } + + return &response[0], nil } } -// SendRequest sends a single RPC request to the server -func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) { +// SendBatch sends a batch of RPC requests to the server +func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (types.RPCResponses, error) { // Create the response channel for the pipeline responseCh := make(chan types.RPCResponses, 1) // Generate a unique request ID hash - requestHash := generateIDHash(request.ID.String()) + requestIDs := make([]string, 0, len(requests)) + + for _, request := range requests { + requestIDs = append(requestIDs, request.ID.String()) + } + + requestHash := generateIDHash(requestIDs...) c.requestMapMux.Lock() c.requestMap[requestHash] = responseCh @@ -111,16 +120,28 @@ func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*ty // Pipe the request to the backlog select { case <-ctx.Done(): - return nil, errTimedOut - case c.backlog <- request: + return nil, ErrTimedOut + case c.backlog <- requests: } // Wait for the response select { case <-ctx.Done(): - return nil, errTimedOut - case response := <-responseCh: - return &response[0], nil + return nil, ErrTimedOut + case responses := <-responseCh: + // Make sure the length matches + if len(responses) != len(requests) { + return nil, ErrInvalidBatchResponse + } + + // Make sure the IDs match + for index, response := range responses { + if requests[index].ID != response.ID { + return nil, ErrRequestResponseIDMismatch + } + } + + return responses, nil } } diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client_test.go b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go index e998c0891c5..c80b98b624f 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client_test.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -// createTestServer creates a test HTTP server +// createTestServer creates a test WS server func createTestServer( t *testing.T, handler http.Handler, @@ -87,7 +87,7 @@ func TestClient_SendRequest(t *testing.T) { response, err := c.SendRequest(ctx, request) require.Nil(t, response) - assert.ErrorIs(t, err, errTimedOut) + assert.ErrorIs(t, err, ErrTimedOut) }) t.Run("valid request sent", func(t *testing.T) { @@ -222,7 +222,7 @@ func TestClient_SendBatch(t *testing.T) { response, err := c.SendBatch(ctx, batch) require.Nil(t, response) - assert.ErrorIs(t, err, errTimedOut) + assert.ErrorIs(t, err, ErrTimedOut) }) t.Run("valid batch sent", func(t *testing.T) { diff --git a/tm2/pkg/bft/rpc/lib/test/main.go b/tm2/pkg/bft/rpc/lib/test/main.go deleted file mode 100644 index 3fd8ea0bf61..00000000000 --- a/tm2/pkg/bft/rpc/lib/test/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "net/http" - - "github.com/gnolang/gno/tm2/pkg/log" - osm "github.com/gnolang/gno/tm2/pkg/os" - - rpcserver "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" - rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" -) - -var routes = map[string]*rpcserver.RPCFunc{ - "hello_world": rpcserver.NewRPCFunc(HelloWorld, "name,num"), -} - -func HelloWorld(ctx *rpctypes.Context, name string, num int) (Result, error) { - return Result{fmt.Sprintf("hi %s %d", name, num)}, nil -} - -type Result struct { - Result string -} - -func main() { - var ( - mux = http.NewServeMux() - logger = log.NewNoopLogger() - ) - - // Stop upon receiving SIGTERM or CTRL-C. - osm.TrapSignal(func() {}) - - rpcserver.RegisterRPCFuncs(mux, routes, logger) - config := rpcserver.DefaultConfig() - listener, err := rpcserver.Listen("0.0.0.0:8008", config) - if err != nil { - osm.Exit(err.Error()) - } - rpcserver.StartHTTPServer(listener, mux, logger, config) -} diff --git a/tm2/pkg/bft/rpc/test/helpers.go b/tm2/pkg/bft/rpc/test/helpers.go deleted file mode 100644 index 7114953607e..00000000000 --- a/tm2/pkg/bft/rpc/test/helpers.go +++ /dev/null @@ -1,145 +0,0 @@ -package rpctest - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "time" - - abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" - cfg "github.com/gnolang/gno/tm2/pkg/bft/config" - nm "github.com/gnolang/gno/tm2/pkg/bft/node" - "github.com/gnolang/gno/tm2/pkg/bft/privval" - "github.com/gnolang/gno/tm2/pkg/bft/proxy" - ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" - "github.com/gnolang/gno/tm2/pkg/log" - "github.com/gnolang/gno/tm2/pkg/p2p" -) - -// Options helps with specifying some parameters for our RPC testing for greater -// control. -type Options struct { - suppressStdout bool - recreateConfig bool -} - -var ( - globalConfig *cfg.Config - defaultOptions = Options{ - recreateConfig: false, - } -) - -func waitForRPC() { - laddr := GetConfig().RPC.ListenAddress - client := rpcclient.NewJSONRPCClient(laddr) - result := new(ctypes.ResultStatus) - for { - err := client.Call("status", map[string]interface{}{}, result) - if err == nil { - return - } else { - fmt.Println("error", err) - time.Sleep(time.Millisecond) - } - } -} - -// f**ing long, but unique for each test -func makePathname() string { - // get path - p, err := os.Getwd() - if err != nil { - panic(err) - } - // fmt.Println(p) - sep := string(filepath.Separator) - return strings.Replace(p, sep, "_", -1) -} - -func createConfig() *cfg.Config { - pathname := makePathname() - c := cfg.ResetTestRoot(pathname) - - // and we use random ports to run in parallel - c.P2P.ListenAddress = "tcp://127.0.0.1:0" - c.RPC.ListenAddress = "tcp://127.0.0.1:0" - c.RPC.CORSAllowedOrigins = []string{"https://tendermint.com/"} - // 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(forceCreate ...bool) *cfg.Config { - if globalConfig == nil || (len(forceCreate) > 0 && forceCreate[0]) { - globalConfig = createConfig() - } - return globalConfig -} - -// StartTendermint starts a test tendermint server in a go routine and returns when it is initialized -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) - } - - // wait for rpc - waitForRPC() - - if !nodeOpts.suppressStdout { - fmt.Println("Tendermint running!") - } - - return node -} - -// StopTendermint stops a test tendermint server, waits until it's stopped and -// cleans up test/config files. -func StopTendermint(node *nm.Node) { - node.Stop() - node.Wait() - os.RemoveAll(node.Config().RootDir) -} - -// NewTendermint creates a new tendermint server and sleeps forever -func NewTendermint(app abci.Application, opts *Options) *nm.Node { - // Create & start node - config := GetConfig(opts.recreateConfig) - - pvKeyFile := config.PrivValidatorKeyFile() - pvKeyStateFile := config.PrivValidatorStateFile() - pv := privval.LoadOrGenFilePV(pvKeyFile, pvKeyStateFile) - papp := proxy.NewLocalClientCreator(app) - nodeKey, err := p2p.LoadOrGenNodeKey(config.NodeKeyFile()) - if err != nil { - panic(err) - } - node, err := nm.NewNode(config, pv, nodeKey, papp, - nm.DefaultGenesisDocProviderFunc(config), - nm.DefaultDBProvider, - log.NewNoopLogger()) - if err != nil { - panic(err) - } - 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 e2febd926a32dc6c056e9cf764aa75b1b55ecb06 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 12 Apr 2024 18:50:37 +0200 Subject: [PATCH 11/26] Implement RPC batch support --- tm2/pkg/bft/rpc/client/batch.go | 401 ++++++++++++++++++++++ tm2/pkg/bft/rpc/client/client.go | 18 +- tm2/pkg/bft/rpc/client/client_test.go | 122 +++++++ tm2/pkg/bft/rpc/client/e2e_test.go | 104 ++++++ tm2/pkg/bft/rpc/lib/client/batch/batch.go | 18 +- 5 files changed, 643 insertions(+), 20 deletions(-) create mode 100644 tm2/pkg/bft/rpc/client/batch.go create mode 100644 tm2/pkg/bft/rpc/client/e2e_test.go diff --git a/tm2/pkg/bft/rpc/client/batch.go b/tm2/pkg/bft/rpc/client/batch.go new file mode 100644 index 00000000000..b1848f8bbeb --- /dev/null +++ b/tm2/pkg/bft/rpc/client/batch.go @@ -0,0 +1,401 @@ +package client + +import ( + "context" + "errors" + "fmt" + "sync" + + "github.com/gnolang/gno/tm2/pkg/amino" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/batch" + rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" +) + +type RPCBatch struct { + batch *batch.Batch + + // resultMap maps the request ID -> result Amino type + // Why? + // There is a weird quirk in this RPC system where request results + // are marshalled into Amino JSON, before being handed off to the client. + // The client, of course, needs to unmarshal the Amino JSON-encoded response result + // back into a concrete type. + // Since working with an RPC batch is asynchronous + // (requests are added at any time, but results are retrieved when the batch is sent) + // there needs to be a record of what specific type the result needs to be Amino unmarshalled to + resultMap map[string]any + + mux sync.RWMutex +} + +func (b *RPCBatch) Count() int { + b.mux.RLock() + defer b.mux.RUnlock() + + return b.batch.Count() +} + +func (b *RPCBatch) Clear() int { + b.mux.Lock() + defer b.mux.Unlock() + + return b.batch.Clear() +} + +func (b *RPCBatch) Send(ctx context.Context) ([]any, error) { + b.mux.Lock() + defer b.mux.Unlock() + + // Save the initial batch size + batchSize := b.batch.Count() + + // Send the batch + responses, err := b.batch.Send(ctx) + if err != nil { + return nil, fmt.Errorf("unable to send RPC batch, %w", err) + } + + var ( + results = make([]any, 0, batchSize) + errs = make([]error, 0, batchSize) + ) + + // Parse the response results + for _, response := range responses { + // Check the error + if response.Error != nil { + errs = append(errs, response.Error) + results = append(results, nil) + + continue + } + + // Get the result type from the result map + result, exists := b.resultMap[response.ID.String()] + if !exists { + return nil, fmt.Errorf("unexpected response with ID %s", response.ID) + } + + // Amino JSON-unmarshal the response result + if err := amino.UnmarshalJSON(response.Result, result); err != nil { + return nil, fmt.Errorf("unable to parse response result, %w", err) + } + + results = append(results, result) + } + + return results, errors.Join(errs...) +} + +func (b *RPCBatch) addRequest(request rpctypes.RPCRequest, result any) { + b.mux.Lock() + defer b.mux.Unlock() + + // Save the result type + b.resultMap[request.ID.String()] = result + + // Add the request to the batch + b.batch.AddRequest(request) +} + +func (b *RPCBatch) Status() error { + // Prepare the RPC request + request, err := newRequest( + statusMethod, + map[string]any{}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultStatus{}) + + return nil +} + +func (b *RPCBatch) ABCIInfo() error { + // Prepare the RPC request + request, err := newRequest( + abciInfoMethod, + map[string]any{}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultABCIInfo{}) + + return nil +} + +func (b *RPCBatch) ABCIQuery(path string, data []byte) error { + return b.ABCIQueryWithOptions(path, data, DefaultABCIQueryOptions) +} + +func (b *RPCBatch) ABCIQueryWithOptions(path string, data []byte, opts ABCIQueryOptions) error { + // Prepare the RPC request + request, err := newRequest( + abciQueryMethod, + map[string]any{ + "path": path, + "data": data, + "height": opts.Height, + "prove": opts.Prove, + }, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultABCIQuery{}) + + return nil +} + +func (b *RPCBatch) BroadcastTxCommit(tx types.Tx) error { + // Prepare the RPC request + request, err := newRequest( + broadcastTxCommitMethod, + map[string]any{"tx": tx}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultBroadcastTxCommit{}) + + return nil +} + +func (b *RPCBatch) BroadcastTxAsync(tx types.Tx) error { + return b.broadcastTX(broadcastTxAsyncMethod, tx) +} + +func (b *RPCBatch) BroadcastTxSync(tx types.Tx) error { + return b.broadcastTX(broadcastTxSyncMethod, tx) +} + +func (b *RPCBatch) broadcastTX(route string, tx types.Tx) error { + // Prepare the RPC request + request, err := newRequest( + route, + map[string]any{"tx": tx}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultBroadcastTx{}) + + return nil +} + +func (b *RPCBatch) UnconfirmedTxs(limit int) error { + // Prepare the RPC request + request, err := newRequest( + unconfirmedTxsMethod, + map[string]any{"limit": limit}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultUnconfirmedTxs{}) + + return nil +} + +func (b *RPCBatch) NumUnconfirmedTxs() error { + // Prepare the RPC request + request, err := newRequest( + numUnconfirmedTxsMethod, + map[string]any{}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultUnconfirmedTxs{}) + + return nil +} + +func (b *RPCBatch) NetInfo() error { + // Prepare the RPC request + request, err := newRequest( + netInfoMethod, + map[string]any{}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultNetInfo{}) + + return nil +} + +func (b *RPCBatch) DumpConsensusState() error { + // Prepare the RPC request + request, err := newRequest( + dumpConsensusStateMethod, + map[string]any{}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultDumpConsensusState{}) + + return nil +} + +func (b *RPCBatch) ConsensusState() error { + // Prepare the RPC request + request, err := newRequest( + consensusStateMethod, + map[string]any{}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultConsensusState{}) + + return nil +} + +func (b *RPCBatch) ConsensusParams(height *int64) error { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + // Prepare the RPC request + request, err := newRequest( + consensusParamsMethod, + params, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultConsensusParams{}) + + return nil +} + +func (b *RPCBatch) Health() error { + // Prepare the RPC request + request, err := newRequest( + healthMethod, + map[string]any{}, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultHealth{}) + + return nil +} + +func (b *RPCBatch) BlockchainInfo(minHeight, maxHeight int64) error { + // Prepare the RPC request + request, err := newRequest( + blockchainMethod, + map[string]any{ + "minHeight": minHeight, + "maxHeight": maxHeight, + }, + ) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultBlockchainInfo{}) + + return nil +} + +func (b *RPCBatch) Genesis() error { + // Prepare the RPC request + request, err := newRequest(genesisMethod, map[string]any{}) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultGenesis{}) + + return nil +} + +func (b *RPCBatch) Block(height *int64) error { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + // Prepare the RPC request + request, err := newRequest(blockMethod, params) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultBlock{}) + + return nil +} + +func (b *RPCBatch) BlockResults(height *int64) error { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + // Prepare the RPC request + request, err := newRequest(blockResultsMethod, params) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultBlockResults{}) + + return nil +} + +func (b *RPCBatch) Commit(height *int64) error { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + // Prepare the RPC request + request, err := newRequest(commitMethod, params) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultCommit{}) + + return nil +} + +func (b *RPCBatch) Validators(height *int64) error { + params := map[string]any{} + if height != nil { + params["height"] = height + } + + // Prepare the RPC request + request, err := newRequest(validatorsMethod, params) + if err != nil { + return fmt.Errorf("unable to create request, %w", err) + } + + b.addRequest(request, &ctypes.ResultValidators{}) + + return nil +} diff --git a/tm2/pkg/bft/rpc/client/client.go b/tm2/pkg/bft/rpc/client/client.go index 6ff5450192a..f31a6af1e9d 100644 --- a/tm2/pkg/bft/rpc/client/client.go +++ b/tm2/pkg/bft/rpc/client/client.go @@ -94,8 +94,11 @@ func NewWSClient(rpcURL string) (*RPCClient, error) { } // NewBatch creates a new RPC batch -func (c *RPCClient) NewBatch() *batch.Batch { - return batch.NewBatch(c.caller) +func (c *RPCClient) NewBatch() *RPCBatch { + return &RPCBatch{ + batch: batch.NewBatch(c.caller), + resultMap: make(map[string]any), + } } func (c *RPCClient) Status() (*ctypes.ResultStatus, error) { @@ -305,6 +308,14 @@ func (c *RPCClient) Validators(height *int64) (*ctypes.ResultValidators, error) ) } +// newRequest creates a new request based on the method +// and given params +func newRequest(method string, params map[string]any) (rpctypes.RPCRequest, error) { + id := rpctypes.JSONRPCStringID(xid.New().String()) + + return rpctypes.MapToRequest(id, method, params) +} + // sendRequestCommon is the common request creation, sending, and parsing middleware func sendRequestCommon[T any]( caller rpcclient.Client, @@ -313,8 +324,7 @@ func sendRequestCommon[T any]( params map[string]any, ) (*T, error) { // Prepare the RPC request - id := rpctypes.JSONRPCStringID(xid.New().String()) - request, err := rpctypes.MapToRequest(id, method, params) + request, err := newRequest(method, params) if err != nil { return nil, err } diff --git a/tm2/pkg/bft/rpc/client/client_test.go b/tm2/pkg/bft/rpc/client/client_test.go index 299b0fee01c..b0ced270e64 100644 --- a/tm2/pkg/bft/rpc/client/client_test.go +++ b/tm2/pkg/bft/rpc/client/client_test.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "testing" + "time" "github.com/gnolang/gno/tm2/pkg/amino" abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" @@ -60,6 +61,54 @@ func generateMockRequestClient( } } +// generateMockRequestsClient generates a batch RPC request mock client +func generateMockRequestsClient( + t *testing.T, + method string, + verifyParamsFn func(*testing.T, map[string]any), + responseData []any, +) *mockClient { + t.Helper() + + return &mockClient{ + sendBatchFn: func( + _ context.Context, + requests types.RPCRequests, + ) (types.RPCResponses, error) { + responses := make(types.RPCResponses, 0, len(requests)) + + // Validate the requests + for index, r := range requests { + require.Equal(t, "2.0", r.JSONRPC) + require.NotNil(t, r.ID) + require.Equal(t, r.Method, method) + + // Validate the params + var params map[string]any + require.NoError(t, json.Unmarshal(r.Params, ¶ms)) + + verifyParamsFn(t, params) + + // Prepare the result + result, err := amino.MarshalJSON(responseData[index]) + require.NoError(t, err) + + // Prepare the response + response := types.RPCResponse{ + JSONRPC: "2.0", + ID: r.ID, + Result: result, + Error: nil, + } + + responses = append(responses, response) + } + + return responses, nil + }, + } +} + func TestRPCClient_Status(t *testing.T) { t.Parallel() @@ -672,3 +721,76 @@ func TestRPCClient_Validators(t *testing.T) { assert.Equal(t, expectedResult, result) } + +func TestRPCClient_Batch(t *testing.T) { + t.Parallel() + + convertResults := func(results []*ctypes.ResultStatus) []any { + res := make([]any, len(results)) + + for index, item := range results { + res[index] = item + } + + return res + } + + var ( + expectedStatuses = []*ctypes.ResultStatus{ + { + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, + }, + { + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, + }, + { + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, + }, + } + + verifyFn = func(t *testing.T, params map[string]any) { + assert.Len(t, params, 0) + } + + mockClient = generateMockRequestsClient( + t, + statusMethod, + verifyFn, + convertResults(expectedStatuses), + ) + ) + + // Create the client + c := NewRPCClient(mockClient) + + // Create the batch + batch := c.NewBatch() + + require.NoError(t, batch.Status()) + require.NoError(t, batch.Status()) + require.NoError(t, batch.Status()) + + require.EqualValues(t, 3, batch.Count()) + + // Send the batch + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + + results, err := batch.Send(ctx) + require.NoError(t, err) + + require.Len(t, results, len(expectedStatuses)) + + for index, result := range results { + castResult, ok := result.(*ctypes.ResultStatus) + require.True(t, ok) + + assert.Equal(t, expectedStatuses[index], castResult) + } +} diff --git a/tm2/pkg/bft/rpc/client/e2e_test.go b/tm2/pkg/bft/rpc/client/e2e_test.go new file mode 100644 index 00000000000..4dfcf8eceef --- /dev/null +++ b/tm2/pkg/bft/rpc/client/e2e_test.go @@ -0,0 +1,104 @@ +package client + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gorilla/websocket" + "github.com/stretchr/testify/require" +) + +// createTestServer creates a test RPC server +func createTestServer( + t *testing.T, + handler http.Handler, +) *httptest.Server { + t.Helper() + + s := httptest.NewServer(handler) + t.Cleanup(s.Close) + + return s +} + +func defaultHTTPHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { + t.Helper() + + return func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + require.Equal(t, "application/json", r.Header.Get("content-type")) + + // Parse the message + var req types.RPCRequest + require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + + // Marshal the result data to Amino JSON + result, err := amino.MarshalJSON(responseBytes) + require.NoError(t, err) + + // Send a response back + response := types.RPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: result, + } + + // Marshal the response + marshalledResponse, err := json.Marshal(response) + require.NoError(t, err) + + _, err = w.Write(marshalledResponse) + require.NoError(t, err) + } +} + +func defaultWSHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { + t.Helper() + + upgrader := websocket.Upgrader{} + + return func(w http.ResponseWriter, r *http.Request) { + c, err := upgrader.Upgrade(w, r, nil) + require.NoError(t, err) + + defer c.Close() + + for { + mt, message, err := c.ReadMessage() + if websocket.IsUnexpectedCloseError(err) { + return + } + + require.NoError(t, err) + + // Parse the message + var req types.RPCRequest + require.NoError(t, json.Unmarshal(message, &req)) + + // Marshal the result data to Amino JSON + result, err := amino.MarshalJSON(responseBytes) + require.NoError(t, err) + + // Send a response back + response := types.RPCResponse{ + JSONRPC: "2.0", + ID: req.ID, + Result: result, + } + + // Marshal the response + marshalledResponse, err := json.Marshal(response) + require.NoError(t, err) + + require.NoError(t, c.WriteMessage(mt, marshalledResponse)) + } + } +} + +func TestRPCClient_E2E_Status(t *testing.T) { + t.Parallel() // TODO implement +} diff --git a/tm2/pkg/bft/rpc/lib/client/batch/batch.go b/tm2/pkg/bft/rpc/lib/client/batch/batch.go index 2cd7ba82bab..e507cd9408f 100644 --- a/tm2/pkg/bft/rpc/lib/client/batch/batch.go +++ b/tm2/pkg/bft/rpc/lib/client/batch/batch.go @@ -2,7 +2,6 @@ package batch import ( "context" - "sync" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" ) @@ -12,11 +11,9 @@ type Client interface { } // Batch 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 +// into a single batch request. +// NOT thread safe type Batch struct { - sync.RWMutex - client Client requests types.RPCRequests } @@ -31,17 +28,11 @@ func NewBatch(client Client) *Batch { // Count returns the number of enqueued requests waiting to be sent func (b *Batch) Count() int { - b.RLock() - defer b.RUnlock() - return len(b.requests) } // Clear empties out the request batch func (b *Batch) Clear() int { - b.Lock() - defer b.Unlock() - return b.clear() } @@ -55,10 +46,8 @@ func (b *Batch) clear() int { // Send will attempt to send the current batch of enqueued requests, and then // will clear out the requests once done func (b *Batch) Send(ctx context.Context) (types.RPCResponses, error) { - b.Lock() defer func() { b.clear() - b.Unlock() }() responses, err := b.client.SendBatch(ctx, b.requests) @@ -71,8 +60,5 @@ func (b *Batch) Send(ctx context.Context) (types.RPCResponses, error) { // AddRequest adds a new request onto the batch func (b *Batch) AddRequest(request types.RPCRequest) { - b.Lock() - defer b.Unlock() - b.requests = append(b.requests, request) } From 86f76dc242b03011175c03bc9678d1200959b6f0 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 12 Apr 2024 19:11:51 +0200 Subject: [PATCH 12/26] Start adding E2E client test cases --- tm2/pkg/bft/rpc/client/client.go | 5 ++ tm2/pkg/bft/rpc/client/e2e_test.go | 91 +++++++++++++++++++++-- tm2/pkg/bft/rpc/client/mock_test.go | 10 +++ tm2/pkg/bft/rpc/lib/client/client.go | 3 + tm2/pkg/bft/rpc/lib/client/http/client.go | 5 ++ 5 files changed, 109 insertions(+), 5 deletions(-) diff --git a/tm2/pkg/bft/rpc/client/client.go b/tm2/pkg/bft/rpc/client/client.go index f31a6af1e9d..3df0576132b 100644 --- a/tm2/pkg/bft/rpc/client/client.go +++ b/tm2/pkg/bft/rpc/client/client.go @@ -93,6 +93,11 @@ func NewWSClient(rpcURL string) (*RPCClient, error) { return NewRPCClient(wsClient), nil } +// Close attempts to gracefully close the RPC client +func (c *RPCClient) Close() error { + return c.caller.Close() +} + // NewBatch creates a new RPC batch func (c *RPCClient) NewBatch() *RPCBatch { return &RPCBatch{ diff --git a/tm2/pkg/bft/rpc/client/e2e_test.go b/tm2/pkg/bft/rpc/client/e2e_test.go index 4dfcf8eceef..ac6694c02c8 100644 --- a/tm2/pkg/bft/rpc/client/e2e_test.go +++ b/tm2/pkg/bft/rpc/client/e2e_test.go @@ -4,11 +4,15 @@ import ( "encoding/json" "net/http" "net/http/httptest" + "strings" "testing" "github.com/gnolang/gno/tm2/pkg/amino" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + "github.com/gnolang/gno/tm2/pkg/p2p" "github.com/gorilla/websocket" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,7 +29,11 @@ func createTestServer( return s } -func defaultHTTPHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { +func defaultHTTPHandler( + t *testing.T, + method string, + responseResult any, +) http.HandlerFunc { t.Helper() return func(w http.ResponseWriter, r *http.Request) { @@ -36,8 +44,12 @@ func defaultHTTPHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { var req types.RPCRequest require.NoError(t, json.NewDecoder(r.Body).Decode(&req)) + // Basic request validation + require.Equal(t, req.JSONRPC, "2.0") + require.Equal(t, req.Method, method) + // Marshal the result data to Amino JSON - result, err := amino.MarshalJSON(responseBytes) + result, err := amino.MarshalJSON(responseResult) require.NoError(t, err) // Send a response back @@ -56,7 +68,11 @@ func defaultHTTPHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { } } -func defaultWSHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { +func defaultWSHandler( + t *testing.T, + method string, + responseResult any, +) http.HandlerFunc { t.Helper() upgrader := websocket.Upgrader{} @@ -79,8 +95,12 @@ func defaultWSHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { var req types.RPCRequest require.NoError(t, json.Unmarshal(message, &req)) + // Basic request validation + require.Equal(t, req.JSONRPC, "2.0") + require.Equal(t, req.Method, method) + // Marshal the result data to Amino JSON - result, err := amino.MarshalJSON(responseBytes) + result, err := amino.MarshalJSON(responseResult) require.NoError(t, err) // Send a response back @@ -99,6 +119,67 @@ func defaultWSHandler(t *testing.T, responseBytes []byte) http.HandlerFunc { } } +type e2eTestCase struct { + name string + client *RPCClient +} + +func generateE2ETestCases( + t *testing.T, + method string, + responseResult any, +) []e2eTestCase { + t.Helper() + + // Create the http client + httpServer := createTestServer(t, defaultHTTPHandler(t, method, responseResult)) + httpClient, err := NewHTTPClient(httpServer.URL) + require.NoError(t, err) + + // Create the WS client + wsServer := createTestServer(t, defaultWSHandler(t, method, responseResult)) + wsClient, err := NewWSClient("ws" + strings.TrimPrefix(wsServer.URL, "http")) + require.NoError(t, err) + + return []e2eTestCase{ + { + name: "http", + client: httpClient, + }, + { + name: "ws", + client: wsClient, + }, + } +} + func TestRPCClient_E2E_Status(t *testing.T) { - t.Parallel() // TODO implement + t.Parallel() + + var ( + expectedStatus = &ctypes.ResultStatus{ + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, + } + ) + + testTable := generateE2ETestCases(t, statusMethod, expectedStatus) + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + defer func() { + require.NoError(t, testCase.client.Close()) + }() + + status, err := testCase.client.Status() + require.NoError(t, err) + + assert.Equal(t, expectedStatus, status) + }) + } } diff --git a/tm2/pkg/bft/rpc/client/mock_test.go b/tm2/pkg/bft/rpc/client/mock_test.go index bc751b4bed0..bc2d92367bc 100644 --- a/tm2/pkg/bft/rpc/client/mock_test.go +++ b/tm2/pkg/bft/rpc/client/mock_test.go @@ -9,11 +9,13 @@ import ( type ( sendRequestDelegate func(context.Context, types.RPCRequest) (*types.RPCResponse, error) sendBatchDelegate func(context.Context, types.RPCRequests) (types.RPCResponses, error) + closeDelegate func() error ) type mockClient struct { sendRequestFn sendRequestDelegate sendBatchFn sendBatchDelegate + closeFn closeDelegate } func (m *mockClient) SendRequest(ctx context.Context, request types.RPCRequest) (*types.RPCResponse, error) { @@ -31,3 +33,11 @@ func (m *mockClient) SendBatch(ctx context.Context, requests types.RPCRequests) return nil, nil } + +func (m *mockClient) Close() error { + if m.closeFn != nil { + return m.closeFn() + } + + return nil +} diff --git a/tm2/pkg/bft/rpc/lib/client/client.go b/tm2/pkg/bft/rpc/lib/client/client.go index d033d811c57..8fc78d9eb64 100644 --- a/tm2/pkg/bft/rpc/lib/client/client.go +++ b/tm2/pkg/bft/rpc/lib/client/client.go @@ -13,6 +13,9 @@ type Client interface { // SendBatch sends a batch of RPC requests to the JSON-RPC layer SendBatch(context.Context, types.RPCRequests) (types.RPCResponses, error) + + // Close closes the RPC client + Close() error } // Batch is the JSON-RPC batch abstraction diff --git a/tm2/pkg/bft/rpc/lib/client/http/client.go b/tm2/pkg/bft/rpc/lib/client/http/client.go index 4e6e760c180..d71cc62faf1 100644 --- a/tm2/pkg/bft/rpc/lib/client/http/client.go +++ b/tm2/pkg/bft/rpc/lib/client/http/client.go @@ -89,6 +89,11 @@ func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (typ return responses, nil } +// Close has no effect on an HTTP client +func (c *Client) Close() error { + return nil +} + type ( requestType interface { types.RPCRequest | types.RPCRequests From fc4f1d7c52c01cafcc7a0ebd65e2da5fe7f8128b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Sun, 14 Apr 2024 20:16:33 +0200 Subject: [PATCH 13/26] Add unit tests for the RPC batch --- tm2/pkg/bft/rpc/client/batch.go | 11 +- tm2/pkg/bft/rpc/client/batch_test.go | 499 +++++++++++++++++++++++++++ tm2/pkg/bft/rpc/config/config.go | 9 + tm2/pkg/bft/rpc/config/utils.go | 11 - 4 files changed, 517 insertions(+), 13 deletions(-) create mode 100644 tm2/pkg/bft/rpc/client/batch_test.go delete mode 100644 tm2/pkg/bft/rpc/config/utils.go diff --git a/tm2/pkg/bft/rpc/client/batch.go b/tm2/pkg/bft/rpc/client/batch.go index b1848f8bbeb..ce2633c15a3 100644 --- a/tm2/pkg/bft/rpc/client/batch.go +++ b/tm2/pkg/bft/rpc/client/batch.go @@ -8,13 +8,15 @@ import ( "github.com/gnolang/gno/tm2/pkg/amino" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" - "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/batch" + rpcclient "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client" rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" "github.com/gnolang/gno/tm2/pkg/bft/types" ) +var errEmptyBatch = errors.New("RPC batch is empty") + type RPCBatch struct { - batch *batch.Batch + batch rpcclient.Batch // resultMap maps the request ID -> result Amino type // Why? @@ -51,6 +53,11 @@ func (b *RPCBatch) Send(ctx context.Context) ([]any, error) { // Save the initial batch size batchSize := b.batch.Count() + // Sanity check for not sending empty batches + if batchSize == 0 { + return nil, errEmptyBatch + } + // Send the batch responses, err := b.batch.Send(ctx) if err != nil { diff --git a/tm2/pkg/bft/rpc/client/batch_test.go b/tm2/pkg/bft/rpc/client/batch_test.go new file mode 100644 index 00000000000..0611db64637 --- /dev/null +++ b/tm2/pkg/bft/rpc/client/batch_test.go @@ -0,0 +1,499 @@ +package client + +import ( + "context" + "testing" + + "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + cstypes "github.com/gnolang/gno/tm2/pkg/bft/consensus/types" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types" + "github.com/gnolang/gno/tm2/pkg/p2p" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// generateMockBatchClient generates a common +// mock batch handling client +func generateMockBatchClient( + t *testing.T, + method string, + expectedRequests int, + commonResult any, +) *mockClient { + t.Helper() + + return &mockClient{ + sendBatchFn: func(_ context.Context, requests types.RPCRequests) (types.RPCResponses, error) { + require.Len(t, requests, expectedRequests) + + responses := make(types.RPCResponses, len(requests)) + + for index, request := range requests { + require.Equal(t, "2.0", request.JSONRPC) + require.NotEmpty(t, request.ID) + require.Equal(t, method, request.Method) + + result, err := amino.MarshalJSON(commonResult) + require.NoError(t, err) + + response := types.RPCResponse{ + JSONRPC: "2.0", + ID: request.ID, + Result: result, + Error: nil, + } + + responses[index] = response + } + + return responses, nil + }, + } +} + +func TestRPCBatch_Count(t *testing.T) { + t.Parallel() + + var ( + c = NewRPCClient(&mockClient{}) + batch = c.NewBatch() + ) + + // Make sure the batch is initially empty + assert.Equal(t, 0, batch.Count()) + + // Add a dummy request + require.NoError(t, batch.Status()) + + // Make sure the request is enqueued + assert.Equal(t, 1, batch.Count()) +} + +func TestRPCBatch_Clear(t *testing.T) { + t.Parallel() + + var ( + c = NewRPCClient(&mockClient{}) + batch = c.NewBatch() + ) + + // Add a dummy request + require.NoError(t, batch.Status()) + + // Make sure the request is enqueued + assert.Equal(t, 1, batch.Count()) + + // Clear the batch + assert.Equal(t, 1, batch.Clear()) + + // Make sure no request is enqueued + assert.Equal(t, 0, batch.Count()) +} + +func TestRPCBatch_Send(t *testing.T) { + t.Parallel() + + t.Run("empty batch", func(t *testing.T) { + t.Parallel() + + var ( + c = NewRPCClient(&mockClient{}) + batch = c.NewBatch() + ) + + res, err := batch.Send(context.Background()) + + assert.ErrorIs(t, err, errEmptyBatch) + assert.Nil(t, res) + }) + + t.Run("valid batch", func(t *testing.T) { + t.Parallel() + + var ( + numRequests = 10 + expectedStatus = &ctypes.ResultStatus{ + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, + } + + mockClient = generateMockBatchClient(t, statusMethod, 10, expectedStatus) + + c = NewRPCClient(mockClient) + batch = c.NewBatch() + ) + + // Enqueue the requests + for i := 0; i < numRequests; i++ { + require.NoError(t, batch.Status()) + } + + // Send the batch + results, err := batch.Send(context.Background()) + require.NoError(t, err) + + // Validate the results + assert.Len(t, results, numRequests) + + for _, result := range results { + castResult, ok := result.(*ctypes.ResultStatus) + require.True(t, ok) + + assert.Equal(t, expectedStatus, castResult) + } + }) +} + +func TestRPCBatch_Endpoints(t *testing.T) { + t.Parallel() + + testTable := []struct { + method string + expectedResult any + batchCallback func(*RPCBatch) + extractCallback func(any) any + }{ + { + statusMethod, + &ctypes.ResultStatus{ + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.Status()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultStatus) + require.True(t, ok) + + return castResult + }, + }, + { + abciInfoMethod, + &ctypes.ResultABCIInfo{ + Response: abci.ResponseInfo{ + LastBlockAppHash: []byte("dummy"), + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.ABCIInfo()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultABCIInfo) + require.True(t, ok) + + return castResult + }, + }, + { + abciQueryMethod, + &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte("dummy"), + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.ABCIQuery("path", []byte("dummy"))) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultABCIQuery) + require.True(t, ok) + + return castResult + }, + }, + { + broadcastTxCommitMethod, + &ctypes.ResultBroadcastTxCommit{ + Hash: []byte("dummy"), + }, + func(batch *RPCBatch) { + require.NoError(t, batch.BroadcastTxCommit([]byte("dummy"))) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultBroadcastTxCommit) + require.True(t, ok) + + return castResult + }, + }, + { + broadcastTxAsyncMethod, + &ctypes.ResultBroadcastTx{ + Hash: []byte("dummy"), + }, + func(batch *RPCBatch) { + require.NoError(t, batch.BroadcastTxAsync([]byte("dummy"))) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultBroadcastTx) + require.True(t, ok) + + return castResult + }, + }, + { + broadcastTxSyncMethod, + &ctypes.ResultBroadcastTx{ + Hash: []byte("dummy"), + }, + func(batch *RPCBatch) { + require.NoError(t, batch.BroadcastTxSync([]byte("dummy"))) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultBroadcastTx) + require.True(t, ok) + + return castResult + }, + }, + { + unconfirmedTxsMethod, + &ctypes.ResultUnconfirmedTxs{ + Count: 10, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.UnconfirmedTxs(0)) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultUnconfirmedTxs) + require.True(t, ok) + + return castResult + }, + }, + { + numUnconfirmedTxsMethod, + &ctypes.ResultUnconfirmedTxs{ + Count: 10, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.NumUnconfirmedTxs()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultUnconfirmedTxs) + require.True(t, ok) + + return castResult + }, + }, + { + netInfoMethod, + &ctypes.ResultNetInfo{ + NPeers: 10, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.NetInfo()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultNetInfo) + require.True(t, ok) + + return castResult + }, + }, + { + dumpConsensusStateMethod, + &ctypes.ResultDumpConsensusState{ + RoundState: &cstypes.RoundState{ + Round: 10, + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.DumpConsensusState()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultDumpConsensusState) + require.True(t, ok) + + return castResult + }, + }, + { + consensusStateMethod, + &ctypes.ResultConsensusState{ + RoundState: cstypes.RoundStateSimple{ + ProposalBlockHash: []byte("dummy"), + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.ConsensusState()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultConsensusState) + require.True(t, ok) + + return castResult + }, + }, + { + consensusParamsMethod, + &ctypes.ResultConsensusParams{ + BlockHeight: 10, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.ConsensusParams(nil)) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultConsensusParams) + require.True(t, ok) + + return castResult + }, + }, + { + healthMethod, + &ctypes.ResultHealth{}, + func(batch *RPCBatch) { + require.NoError(t, batch.Health()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultHealth) + require.True(t, ok) + + return castResult + }, + }, + { + blockchainMethod, + &ctypes.ResultBlockchainInfo{ + LastHeight: 100, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.BlockchainInfo(0, 0)) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultBlockchainInfo) + require.True(t, ok) + + return castResult + }, + }, + { + genesisMethod, + &ctypes.ResultGenesis{ + Genesis: &bfttypes.GenesisDoc{ + ChainID: "dummy", + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.Genesis()) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultGenesis) + require.True(t, ok) + + return castResult + }, + }, + { + blockMethod, + &ctypes.ResultBlock{ + BlockMeta: &bfttypes.BlockMeta{ + Header: bfttypes.Header{ + Height: 10, + }, + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.Block(nil)) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultBlock) + require.True(t, ok) + + return castResult + }, + }, + { + blockResultsMethod, + &ctypes.ResultBlockResults{ + Height: 10, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.BlockResults(nil)) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultBlockResults) + require.True(t, ok) + + return castResult + }, + }, + { + commitMethod, + &ctypes.ResultCommit{ + CanonicalCommit: true, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.Commit(nil)) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultCommit) + require.True(t, ok) + + return castResult + }, + }, + { + validatorsMethod, + &ctypes.ResultValidators{ + BlockHeight: 10, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.Validators(nil)) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultValidators) + require.True(t, ok) + + return castResult + }, + }, + } + + for _, testCase := range testTable { + testCase := testCase + + t.Run(testCase.method, func(t *testing.T) { + t.Parallel() + + var ( + numRequests = 10 + mockClient = generateMockBatchClient( + t, + testCase.method, + numRequests, + testCase.expectedResult, + ) + + c = NewRPCClient(mockClient) + batch = c.NewBatch() + ) + + // Enqueue the requests + for i := 0; i < numRequests; i++ { + testCase.batchCallback(batch) + } + + // Send the batch + results, err := batch.Send(context.Background()) + require.NoError(t, err) + + // Validate the results + assert.Len(t, results, numRequests) + + for _, result := range results { + castResult := testCase.extractCallback(result) + + assert.Equal(t, testCase.expectedResult, castResult) + } + }) + } +} diff --git a/tm2/pkg/bft/rpc/config/config.go b/tm2/pkg/bft/rpc/config/config.go index 76c490bf94c..1428861626c 100644 --- a/tm2/pkg/bft/rpc/config/config.go +++ b/tm2/pkg/bft/rpc/config/config.go @@ -163,3 +163,12 @@ func (cfg RPCConfig) CertFile() string { func (cfg RPCConfig) IsTLSEnabled() bool { return cfg.TLSCertFile != "" && cfg.TLSKeyFile != "" } + +// helper function to make config creation independent of root dir +func join(root, path string) string { + if filepath.IsAbs(path) { + return path + } + + return filepath.Join(root, path) +} diff --git a/tm2/pkg/bft/rpc/config/utils.go b/tm2/pkg/bft/rpc/config/utils.go deleted file mode 100644 index 5a6eec09e43..00000000000 --- a/tm2/pkg/bft/rpc/config/utils.go +++ /dev/null @@ -1,11 +0,0 @@ -package config - -import "path/filepath" - -// helper function to make config creation independent of root dir -func join(root, path string) string { - if filepath.IsAbs(path) { - return path - } - return filepath.Join(root, path) -} From 5de0bead5c531f7ac6d69cae5c02b5f79abf639c Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Apr 2024 10:53:41 +0200 Subject: [PATCH 14/26] Add E2E RPC client tests --- tm2/pkg/bft/rpc/client/e2e_test.go | 284 +++++++++++++++++++++++++++-- 1 file changed, 270 insertions(+), 14 deletions(-) diff --git a/tm2/pkg/bft/rpc/client/e2e_test.go b/tm2/pkg/bft/rpc/client/e2e_test.go index ac6694c02c8..89984f62b11 100644 --- a/tm2/pkg/bft/rpc/client/e2e_test.go +++ b/tm2/pkg/bft/rpc/client/e2e_test.go @@ -8,8 +8,11 @@ import ( "testing" "github.com/gnolang/gno/tm2/pkg/amino" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + cstypes "github.com/gnolang/gno/tm2/pkg/bft/consensus/types" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" + bfttypes "github.com/gnolang/gno/tm2/pkg/bft/types" "github.com/gnolang/gno/tm2/pkg/p2p" "github.com/gorilla/websocket" "github.com/stretchr/testify/assert" @@ -29,6 +32,7 @@ func createTestServer( return s } +// defaultHTTPHandler generates a default HTTP test handler func defaultHTTPHandler( t *testing.T, method string, @@ -68,6 +72,7 @@ func defaultHTTPHandler( } } +// defaultWSHandler generates a default WS test handler func defaultWSHandler( t *testing.T, method string, @@ -124,6 +129,7 @@ type e2eTestCase struct { client *RPCClient } +// generateE2ETestCases generates RPC client test cases (HTTP / WS) func generateE2ETestCases( t *testing.T, method string, @@ -153,18 +159,257 @@ func generateE2ETestCases( } } -func TestRPCClient_E2E_Status(t *testing.T) { +func TestRPCClient_E2E_Endpoints(t *testing.T) { t.Parallel() - var ( - expectedStatus = &ctypes.ResultStatus{ - NodeInfo: p2p.NodeInfo{ - Moniker: "dummy", + testTable := []struct { + name string + expectedResult any + verifyFn func(*RPCClient, any) + }{ + { + statusMethod, + &ctypes.ResultStatus{ + NodeInfo: p2p.NodeInfo{ + Moniker: "dummy", + }, }, - } - ) + func(client *RPCClient, expectedResult any) { + status, err := client.Status() + require.NoError(t, err) + + assert.Equal(t, expectedResult, status) + }, + }, + { + abciInfoMethod, + &ctypes.ResultABCIInfo{ + Response: abci.ResponseInfo{ + LastBlockAppHash: []byte("dummy"), + }, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.ABCIInfo() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + abciQueryMethod, + &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte("dummy"), + }, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.ABCIQuery("path", []byte("dummy")) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + broadcastTxCommitMethod, + &ctypes.ResultBroadcastTxCommit{ + Hash: []byte("dummy"), + }, + func(client *RPCClient, expectedResult any) { + result, err := client.BroadcastTxCommit([]byte("dummy")) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + broadcastTxAsyncMethod, + &ctypes.ResultBroadcastTx{ + Hash: []byte("dummy"), + }, + func(client *RPCClient, expectedResult any) { + result, err := client.BroadcastTxAsync([]byte("dummy")) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + broadcastTxSyncMethod, + &ctypes.ResultBroadcastTx{ + Hash: []byte("dummy"), + }, + func(client *RPCClient, expectedResult any) { + result, err := client.BroadcastTxSync([]byte("dummy")) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + unconfirmedTxsMethod, + &ctypes.ResultUnconfirmedTxs{ + Count: 10, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.UnconfirmedTxs(0) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + numUnconfirmedTxsMethod, + &ctypes.ResultUnconfirmedTxs{ + Count: 10, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.NumUnconfirmedTxs() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + netInfoMethod, + &ctypes.ResultNetInfo{ + NPeers: 10, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.NetInfo() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + dumpConsensusStateMethod, + &ctypes.ResultDumpConsensusState{ + RoundState: &cstypes.RoundState{ + Round: 10, + }, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.DumpConsensusState() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + consensusStateMethod, + &ctypes.ResultConsensusState{ + RoundState: cstypes.RoundStateSimple{ + ProposalBlockHash: []byte("dummy"), + }, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.ConsensusState() + require.NoError(t, err) - testTable := generateE2ETestCases(t, statusMethod, expectedStatus) + assert.Equal(t, expectedResult, result) + }, + }, + { + consensusParamsMethod, + &ctypes.ResultConsensusParams{ + BlockHeight: 10, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.ConsensusParams(nil) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + healthMethod, + &ctypes.ResultHealth{}, + func(client *RPCClient, expectedResult any) { + result, err := client.Health() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + blockchainMethod, + &ctypes.ResultBlockchainInfo{ + LastHeight: 100, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.BlockchainInfo(0, 0) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + genesisMethod, + &ctypes.ResultGenesis{ + Genesis: &bfttypes.GenesisDoc{ + ChainID: "dummy", + }, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.Genesis() + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + blockMethod, + &ctypes.ResultBlock{ + BlockMeta: &bfttypes.BlockMeta{ + Header: bfttypes.Header{ + Height: 10, + }, + }, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.Block(nil) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + blockResultsMethod, + &ctypes.ResultBlockResults{ + Height: 10, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.BlockResults(nil) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + commitMethod, + &ctypes.ResultCommit{ + CanonicalCommit: true, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.Commit(nil) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + { + validatorsMethod, + &ctypes.ResultValidators{ + BlockHeight: 10, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.Validators(nil) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + }, + }, + } for _, testCase := range testTable { testCase := testCase @@ -172,14 +417,25 @@ func TestRPCClient_E2E_Status(t *testing.T) { t.Run(testCase.name, func(t *testing.T) { t.Parallel() - defer func() { - require.NoError(t, testCase.client.Close()) - }() + clientTable := generateE2ETestCases( + t, + testCase.name, + testCase.expectedResult, + ) - status, err := testCase.client.Status() - require.NoError(t, err) + for _, clientCase := range clientTable { + clientCase := clientCase - assert.Equal(t, expectedStatus, status) + t.Run(clientCase.name, func(t *testing.T) { + t.Parallel() + + defer func() { + require.NoError(t, clientCase.client.Close()) + }() + + testCase.verifyFn(clientCase.client, testCase.expectedResult) + }) + } }) } } From 8977f83a1c4ece280000e768379018f5ed03fe26 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Apr 2024 11:38:40 +0200 Subject: [PATCH 15/26] Clean up rpctypes tests --- tm2/pkg/bft/rpc/lib/rpc_test.go | 166 --------------------- tm2/pkg/bft/rpc/lib/types/types.go | 76 +++++----- tm2/pkg/bft/rpc/lib/types/types_test.go | 183 +++++++++++++++--------- 3 files changed, 161 insertions(+), 264 deletions(-) delete mode 100644 tm2/pkg/bft/rpc/lib/rpc_test.go diff --git a/tm2/pkg/bft/rpc/lib/rpc_test.go b/tm2/pkg/bft/rpc/lib/rpc_test.go deleted file mode 100644 index 95fe5e362be..00000000000 --- a/tm2/pkg/bft/rpc/lib/rpc_test.go +++ /dev/null @@ -1,166 +0,0 @@ -package rpc - -import ( - "net/http" - "os" - "os/exec" - "testing" - "time" - - http2 "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/http" - client "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/client/uri" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - server "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/server" - types "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" - "github.com/gnolang/gno/tm2/pkg/log" -) - -// Client and Server should work over tcp or unix sockets -const ( - tcpAddr = "tcp://0.0.0.0:47768" - tcpServerUnavailableAddr = "tcp://0.0.0.0:47769" - - unixSocket = "/tmp/rpc_test.sock" - unixAddr = "unix://" + unixSocket - - websocketEndpoint = "/websocket/endpoint" -) - -type ResultEcho struct { - Value string `json:"value"` -} - -type ResultEchoInt struct { - Value int `json:"value"` -} - -type ResultEchoBytes struct { - Value []byte `json:"value"` -} - -type ResultEchoDataBytes struct { - Value []byte `json:"value"` -} - -// Define some routes -var Routes = map[string]*server.RPCFunc{ - "echo": server.NewRPCFunc(EchoResult, "arg"), - "echo_ws": server.NewWSRPCFunc(EchoWSResult, "arg"), - "echo_bytes": server.NewRPCFunc(EchoBytesResult, "arg"), - "echo_data_bytes": server.NewRPCFunc(EchoDataBytesResult, "arg"), - "echo_int": server.NewRPCFunc(EchoIntResult, "arg"), -} - -func EchoResult(ctx *types.Context, v string) (*ResultEcho, error) { - return &ResultEcho{v}, nil -} - -func EchoWSResult(ctx *types.Context, v string) (*ResultEcho, error) { - return &ResultEcho{v}, nil -} - -func EchoIntResult(ctx *types.Context, v int) (*ResultEchoInt, error) { - return &ResultEchoInt{v}, nil -} - -func EchoBytesResult(ctx *types.Context, v []byte) (*ResultEchoBytes, error) { - return &ResultEchoBytes{v}, nil -} - -func EchoDataBytesResult(ctx *types.Context, v []byte) (*ResultEchoDataBytes, error) { - return &ResultEchoDataBytes{v}, nil -} - -func TestMain(m *testing.M) { - setup() - code := m.Run() - os.Exit(code) -} - -// launch unix and tcp servers -func setup() { - logger := log.NewNoopLogger() - - cmd := exec.Command("rm", "-f", unixSocket) - err := cmd.Start() - if err != nil { - panic(err) - } - if err = cmd.Wait(); err != nil { - panic(err) - } - - tcpLogger := logger.With("socket", "tcp") - mux := http.NewServeMux() - server.RegisterRPCFuncs(mux, Routes, tcpLogger) - wm := server.NewWebsocketManager(Routes, server.ReadWait(5*time.Second), server.PingPeriod(1*time.Second)) - wm.SetLogger(tcpLogger) - mux.HandleFunc(websocketEndpoint, wm.WebsocketHandler) - config := server.DefaultConfig() - listener1, err := server.Listen(tcpAddr, config) - if err != nil { - panic(err) - } - go server.StartHTTPServer(listener1, mux, tcpLogger, config) - - unixLogger := logger.With("socket", "unix") - mux2 := http.NewServeMux() - server.RegisterRPCFuncs(mux2, Routes, unixLogger) - wm = server.NewWebsocketManager(Routes) - wm.SetLogger(unixLogger) - mux2.HandleFunc(websocketEndpoint, wm.WebsocketHandler) - listener2, err := server.Listen(unixAddr, config) - if err != nil { - panic(err) - } - go server.StartHTTPServer(listener2, mux2, unixLogger, config) - - listener3, err := server.Listen(tcpServerUnavailableAddr, config) - if err != nil { - panic(err) - } - mux3 := http.NewServeMux() - mux3.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { - http.Error(w, "oups", http.StatusTeapot) - }) - go server.StartHTTPServer(listener3, mux3, tcpLogger, config) - - // wait for servers to start - time.Sleep(time.Second * 2) -} - -func echoViaHTTP(cl http2.HTTPClient, val string) (string, error) { - params := map[string]interface{}{ - "arg": val, - } - result := new(ResultEcho) - if err := cl.Call("echo", params, result); err != nil { - return "", err - } - return result.Value, nil -} - -// ------------- -func TestHexStringArg(t *testing.T) { - t.Parallel() - - cl := client.NewClient(tcpAddr) - // should NOT be handled as hex - val := "0xabc" - got, err := echoViaHTTP(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) -} - -func TestQuotedStringArg(t *testing.T) { - t.Parallel() - - cl := client.NewClient(tcpAddr) - // should NOT be unquoted - val := "\"abc\"" - got, err := echoViaHTTP(cl, val) - require.Nil(t, err) - assert.Equal(t, got, val) -} diff --git a/tm2/pkg/bft/rpc/lib/types/types.go b/tm2/pkg/bft/rpc/lib/types/types.go index e92ee7d49f5..e1d165e6e54 100644 --- a/tm2/pkg/bft/rpc/lib/types/types.go +++ b/tm2/pkg/bft/rpc/lib/types/types.go @@ -11,18 +11,15 @@ import ( "github.com/gnolang/gno/tm2/pkg/errors" ) -// a wrapper to emulate a sum type: JSONRPCID = string | int -// TODO: refactor when Go 2.0 arrives https://github.com/golang/go/issues/19412 +// JSONRPCID is a wrapper type for JSON-RPC request IDs, +// which can be a string value | number value | not set (nil) type JSONRPCID interface { - IsJSONRPCID() String() string } // JSONRPCStringID a wrapper for JSON-RPC string IDs type JSONRPCStringID string -func (JSONRPCStringID) IsJSONRPCID() {} - func (id JSONRPCStringID) String() string { return string(id) } @@ -30,14 +27,13 @@ func (id JSONRPCStringID) String() string { // JSONRPCIntID a wrapper for JSON-RPC integer IDs type JSONRPCIntID int -func (JSONRPCIntID) IsJSONRPCID() {} - func (id JSONRPCIntID) String() string { return fmt.Sprintf("%d", id) } -func idFromInterface(idInterface interface{}) (JSONRPCID, error) { - switch id := idInterface.(type) { +// parseID parses the given ID value +func parseID(idValue any) (JSONRPCID, error) { + switch id := idValue.(type) { case string: return JSONRPCStringID(id), nil case float64: @@ -66,25 +62,32 @@ type RPCRequest struct { func (request *RPCRequest) UnmarshalJSON(data []byte) error { unsafeReq := &struct { JSONRPC string `json:"jsonrpc"` - ID interface{} `json:"id"` + ID any `json:"id"` Method string `json:"method"` - Params json.RawMessage `json:"params"` // must be map[string]interface{} or []interface{} + Params json.RawMessage `json:"params"` // must be map[string]any or []any }{} - err := json.Unmarshal(data, &unsafeReq) - if err != nil { - return err + + if err := json.Unmarshal(data, &unsafeReq); err != nil { + return fmt.Errorf("unable to JSON-parse the RPC request, %w", err) } + request.JSONRPC = unsafeReq.JSONRPC request.Method = unsafeReq.Method request.Params = unsafeReq.Params + + // Check if the ID is set if unsafeReq.ID == nil { return nil } - id, err := idFromInterface(unsafeReq.ID) + + // Parse the ID + id, err := parseID(unsafeReq.ID) if err != nil { - return err + return fmt.Errorf("unable to parse request ID, %w", err) } + request.ID = id + return nil } @@ -101,21 +104,25 @@ func (request RPCRequest) String() string { return fmt.Sprintf("[%s %s]", request.ID, request.Method) } -func MapToRequest(id JSONRPCID, method string, params map[string]interface{}) (RPCRequest, error) { +// MapToRequest generates an RPC request with the given ID and method. +// The params are encoded as a JSON map +func MapToRequest(id JSONRPCID, method string, params map[string]any) (RPCRequest, error) { params_ := make(map[string]json.RawMessage, len(params)) for name, value := range params { valueJSON, err := amino.MarshalJSON(value) if err != nil { - return RPCRequest{}, err + return RPCRequest{}, fmt.Errorf("unable to parse param, %w", err) } + params_[name] = valueJSON } + payload, err := json.Marshal(params_) // NOTE: Amino doesn't handle maps yet. if err != nil { - return RPCRequest{}, err + return RPCRequest{}, fmt.Errorf("unable to JSON marshal params, %w", err) } - request := NewRPCRequest(id, method, payload) - return request, nil + + return NewRPCRequest(id, method, payload), nil } // ---------------------------------------- @@ -128,10 +135,11 @@ type RPCError struct { } func (err RPCError) Error() string { - const baseFormat = "RPC error %v - %s" + const baseFormat = "RPC error %d - %s" if err.Data != "" { return fmt.Sprintf(baseFormat+": %s", err.Code, err.Message, err.Data) } + return fmt.Sprintf(baseFormat, err.Code, err.Message) } @@ -155,25 +163,33 @@ func (response *RPCResponse) UnmarshalJSON(data []byte) error { Result json.RawMessage `json:"result,omitempty"` Error *RPCError `json:"error,omitempty"` }{} - err := json.Unmarshal(data, &unsafeResp) - if err != nil { - return err + + // Parse the response + if err := json.Unmarshal(data, &unsafeResp); err != nil { + return fmt.Errorf("unable to JSON-parse the RPC response, %w", err) } + response.JSONRPC = unsafeResp.JSONRPC response.Error = unsafeResp.Error response.Result = unsafeResp.Result + + // Check if any response ID is set if unsafeResp.ID == nil { return nil } - id, err := idFromInterface(unsafeResp.ID) + + // Parse the ID + id, err := parseID(unsafeResp.ID) if err != nil { - return err + return fmt.Errorf("unable to parse response ID, %w", err) } + response.ID = id + return nil } -func NewRPCSuccessResponse(id JSONRPCID, res interface{}) RPCResponse { +func NewRPCSuccessResponse(id JSONRPCID, res any) RPCResponse { var rawMsg json.RawMessage if res != nil { @@ -223,10 +239,6 @@ func RPCInternalError(id JSONRPCID, err error) RPCResponse { return NewRPCErrorResponse(id, -32603, "Internal error", err.Error()) } -func RPCServerError(id JSONRPCID, err error) RPCResponse { - return NewRPCErrorResponse(id, -32000, "Server error", err.Error()) -} - // ---------------------------------------- // WSRPCConnection represents a websocket connection. diff --git a/tm2/pkg/bft/rpc/lib/types/types_test.go b/tm2/pkg/bft/rpc/lib/types/types_test.go index 367ec83bfa3..ff50c1b6c15 100644 --- a/tm2/pkg/bft/rpc/lib/types/types_test.go +++ b/tm2/pkg/bft/rpc/lib/types/types_test.go @@ -6,82 +6,133 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/gnolang/gno/tm2/pkg/errors" ) -type SampleResult struct { - Value string -} +func TestJSONRPCID_Marshal_Unmarshal(t *testing.T) { + t.Parallel() -type responseTest struct { - id JSONRPCID - expected string -} + testTable := []struct { + name string + id JSONRPCID + expectedID string + }{ + { + "short string", + JSONRPCStringID("1"), + `"1"`, + }, + { + "long string", + JSONRPCStringID("alphabet"), + `"alphabet"`, + }, + { + "empty string", + JSONRPCStringID(""), + `""`, + }, + { + "unicode string", + JSONRPCStringID("àáâ"), + `"àáâ"`, + }, + { + "negative number", + JSONRPCIntID(-1), + "-1", + }, + { + "zero ID", + JSONRPCIntID(0), + "0", + }, + { + "non-zero ID", + JSONRPCIntID(100), + "100", + }, + } -var responseTests = []responseTest{ - {JSONRPCStringID("1"), `"1"`}, - {JSONRPCStringID("alphabet"), `"alphabet"`}, - {JSONRPCStringID(""), `""`}, - {JSONRPCStringID("àáâ"), `"àáâ"`}, - {JSONRPCIntID(-1), "-1"}, - {JSONRPCIntID(0), "0"}, - {JSONRPCIntID(1), "1"}, - {JSONRPCIntID(100), "100"}, -} + for _, testCase := range testTable { + testCase := testCase -func TestResponses(t *testing.T) { - t.Parallel() + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() - assert := assert.New(t) - for _, tt := range responseTests { - jsonid := tt.id - a := NewRPCSuccessResponse(jsonid, &SampleResult{"hello"}) - b, _ := json.Marshal(a) - s := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected) - assert.Equal(s, string(b)) - - d := RPCParseError(jsonid, errors.New("Hello world")) - e, _ := json.Marshal(d) - f := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}`, tt.expected) - assert.Equal(f, string(e)) - - g := RPCMethodNotFoundError(jsonid) - h, _ := json.Marshal(g) - i := fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"error":{"code":-32601,"message":"Method not found"}}`, tt.expected) - assert.Equal(string(h), i) - } -} + t.Run("marshal", func(t *testing.T) { + t.Parallel() -func TestUnmarshallResponses(t *testing.T) { - t.Parallel() + data, err := json.Marshal( + NewRPCSuccessResponse(testCase.id, struct { + Value string + }{ + Value: "hello", + }, + ), + ) + require.NoError(t, err) - assert := assert.New(t) - for _, tt := range responseTests { - response := &RPCResponse{} - err := json.Unmarshal([]byte(fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, tt.expected)), response) - assert.Nil(err) - a := NewRPCSuccessResponse(tt.id, &SampleResult{"hello"}) - assert.Equal(*response, a) - } - response := &RPCResponse{} - err := json.Unmarshal([]byte(`{"jsonrpc":"2.0","id":true,"result":{"Value":"hello"}}`), response) - assert.NotNil(err) -} + assert.Equal( + t, + fmt.Sprintf( + `{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, + testCase.expectedID, + ), + string(data), + ) -func TestRPCError(t *testing.T) { - t.Parallel() + data, err = json.Marshal(RPCParseError(testCase.id, errors.New("Hello world"))) + require.NoError(t, err) + + assert.Equal( + t, + fmt.Sprintf( + `{"jsonrpc":"2.0","id":%v,"error":{"code":-32700,"message":"Parse error. Invalid JSON","data":"Hello world"}}`, + testCase.expectedID, + ), + string(data), + ) + + data, err = json.Marshal(RPCMethodNotFoundError(testCase.id)) + require.NoError(t, err) + + assert.Equal( + t, + fmt.Sprintf( + `{"jsonrpc":"2.0","id":%v,"error":{"code":-32601,"message":"Method not found"}}`, + testCase.expectedID, + ), + string(data), + ) + }) - assert.Equal(t, "RPC error 12 - Badness: One worse than a code 11", - fmt.Sprintf("%v", &RPCError{ - Code: 12, - Message: "Badness", - Data: "One worse than a code 11", - })) - - assert.Equal(t, "RPC error 12 - Badness", - fmt.Sprintf("%v", &RPCError{ - Code: 12, - Message: "Badness", - })) + t.Run("unmarshal", func(t *testing.T) { + t.Parallel() + + var expectedResponse RPCResponse + + assert.NoError( + t, + json.Unmarshal( + []byte(fmt.Sprintf(`{"jsonrpc":"2.0","id":%v,"result":{"Value":"hello"}}`, testCase.expectedID)), + &expectedResponse, + ), + ) + + successResponse := NewRPCSuccessResponse( + testCase.id, + struct { + Value string + }{ + Value: "hello", + }, + ) + + assert.Equal(t, expectedResponse, successResponse) + }) + }) + } } From 4b6f5c24b7c46e4aec99902031b759afe590408b Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Mon, 15 Apr 2024 14:32:47 +0200 Subject: [PATCH 16/26] Resolve codebase linting errors --- contribs/gnodev/go.mod | 1 + contribs/gnodev/go.sum | 2 ++ contribs/gnokeykc/go.mod | 1 + contribs/gnokeykc/go.sum | 4 +-- gno.land/pkg/gnoclient/example_test.go | 6 ++-- gno.land/pkg/gnoclient/integration_test.go | 28 +++++++++------ gno.land/pkg/gnoweb/gnoweb.go | 6 +++- gnovm/pkg/gnomod/fetch.go | 6 +++- go.mod | 9 +++-- go.sum | 8 ++--- tm2/pkg/bft/rpc/client/client_test.go | 40 ++++++++++++++++++++++ tm2/pkg/crypto/keys/client/broadcast.go | 5 ++- tm2/pkg/crypto/keys/client/query.go | 6 +++- 13 files changed, 94 insertions(+), 28 deletions(-) diff --git a/contribs/gnodev/go.mod b/contribs/gnodev/go.mod index 4ec6a63d59c..fa4e639cf05 100644 --- a/contribs/gnodev/go.mod +++ b/contribs/gnodev/go.mod @@ -36,6 +36,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/rs/cors v1.10.1 // indirect + github.com/rs/xid v1.5.0 // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect diff --git a/contribs/gnodev/go.sum b/contribs/gnodev/go.sum index a20a723cc14..526e88ed2de 100644 --- a/contribs/gnodev/go.sum +++ b/contribs/gnodev/go.sum @@ -126,6 +126,8 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/contribs/gnokeykc/go.mod b/contribs/gnokeykc/go.mod index 66a3c750633..dea4afde8df 100644 --- a/contribs/gnokeykc/go.mod +++ b/contribs/gnokeykc/go.mod @@ -29,6 +29,7 @@ require ( github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/xid v1.5.0 // indirect github.com/stretchr/testify v1.9.0 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/zondax/hid v0.9.2 // indirect diff --git a/contribs/gnokeykc/go.sum b/contribs/gnokeykc/go.sum index 1bc8ea08a57..aa01ce333a9 100644 --- a/contribs/gnokeykc/go.sum +++ b/contribs/gnokeykc/go.sum @@ -112,8 +112,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.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/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= -github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= diff --git a/gno.land/pkg/gnoclient/example_test.go b/gno.land/pkg/gnoclient/example_test.go index a6a711bf0a2..1ac3cf17cb0 100644 --- a/gno.land/pkg/gnoclient/example_test.go +++ b/gno.land/pkg/gnoclient/example_test.go @@ -16,7 +16,7 @@ func Example_withDisk() { } remote := "127.0.0.1:26657" - rpcClient := rpcclient.NewHTTP(remote) + rpcClient, _ := rpcclient.NewHTTPClient(remote) client := gnoclient.Client{ Signer: signer, @@ -35,7 +35,7 @@ func Example_withInMemCrypto() { signer, _ := gnoclient.SignerFromBip39(mnemo, chainID, bip39Passphrase, account, index) remote := "127.0.0.1:26657" - rpcClient := rpcclient.NewHTTP(remote) + rpcClient, _ := rpcclient.NewHTTPClient(remote) client := gnoclient.Client{ Signer: signer, @@ -47,7 +47,7 @@ func Example_withInMemCrypto() { // Example_readOnly demonstrates how to initialize a read-only gnoclient, which can only query. func Example_readOnly() { remote := "127.0.0.1:26657" - rpcClient := rpcclient.NewHTTP(remote) + rpcClient, _ := rpcclient.NewHTTPClient(remote) client := gnoclient.Client{ RPCClient: rpcClient, diff --git a/gno.land/pkg/gnoclient/integration_test.go b/gno.land/pkg/gnoclient/integration_test.go index a35ffb6fab3..ace9022e35d 100644 --- a/gno.land/pkg/gnoclient/integration_test.go +++ b/gno.land/pkg/gnoclient/integration_test.go @@ -25,7 +25,8 @@ func TestCallSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) // Setup Client client := Client{ @@ -68,7 +69,8 @@ func TestCallMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) // Setup Client client := Client{ @@ -119,7 +121,8 @@ func TestSendSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) // Setup Client client := Client{ @@ -167,7 +170,8 @@ func TestSendMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) // Setup Client client := Client{ @@ -223,7 +227,8 @@ func TestRunSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) client := Client{ Signer: signer, @@ -281,7 +286,8 @@ func TestRunMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) client := Client{ Signer: signer, @@ -361,7 +367,8 @@ func TestAddPackageSingle_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) // Setup Client client := Client{ @@ -404,7 +411,7 @@ func Echo(str string) string { } // Execute AddPackage - _, err := client.AddPackage(baseCfg, msg) + _, err = client.AddPackage(baseCfg, msg) assert.Nil(t, err) // Check for deployed file on the node @@ -429,7 +436,8 @@ func TestAddPackageMultiple_Integration(t *testing.T) { // Init Signer & RPCClient signer := newInMemorySigner(t, "tendermint_test") - rpcClient := rpcclient.NewHTTP(remoteAddr) + rpcClient, err := rpcclient.NewHTTPClient(remoteAddr) + require.NoError(t, err) // Setup Client client := Client{ @@ -495,7 +503,7 @@ func Hello(str string) string { } // Execute AddPackage - _, err := client.AddPackage(baseCfg, msg1, msg2) + _, err = client.AddPackage(baseCfg, msg1, msg2) assert.Nil(t, err) // Check Package #1 diff --git a/gno.land/pkg/gnoweb/gnoweb.go b/gno.land/pkg/gnoweb/gnoweb.go index 5c1c8feb424..13c9f8ac2de 100644 --- a/gno.land/pkg/gnoweb/gnoweb.go +++ b/gno.land/pkg/gnoweb/gnoweb.go @@ -421,7 +421,11 @@ func makeRequest(log *slog.Logger, cfg *Config, qpath string, data []byte) (res // Prove: false, XXX } remote := cfg.RemoteAddr - cli := client.NewHTTP(remote) + cli, err := client.NewHTTPClient(remote) + if err != nil { + return nil, fmt.Errorf("unable to create HTTP client, %w", err) + } + qres, err := cli.ABCIQueryWithOptions( qpath, data, opts2) if err != nil { diff --git a/gnovm/pkg/gnomod/fetch.go b/gnovm/pkg/gnomod/fetch.go index 1dd11846986..24aaac2f9d4 100644 --- a/gnovm/pkg/gnomod/fetch.go +++ b/gnovm/pkg/gnomod/fetch.go @@ -12,7 +12,11 @@ func queryChain(remote string, qpath string, data []byte) (res *abci.ResponseQue // Height: height, XXX // Prove: false, XXX } - cli := client.NewHTTP(remote) + cli, err := client.NewHTTPClient(remote) + if err != nil { + return nil, err + } + qres, err := cli.ABCIQueryWithOptions(qpath, data, opts2) if err != nil { return nil, err diff --git a/go.mod b/go.mod index cefc56df62c..6e689ccf874 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.21 require ( dario.cat/mergo v1.0.0 + github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/btcsuite/btcd/btcutil v1.1.5 github.com/cockroachdb/apd/v3 v3.2.1 github.com/cosmos/ledger-cosmos-go v0.13.3 @@ -11,7 +12,7 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 github.com/fortytw2/leaktest v1.3.0 github.com/gdamore/tcell/v2 v2.7.4 - github.com/gnolang/faucet v0.1.3 + github.com/gnolang/faucet v0.2.0 github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 github.com/golang/protobuf v1.5.4 github.com/google/gofuzz v1.2.0 @@ -28,6 +29,7 @@ require ( github.com/pmezard/go-difflib v1.0.0 github.com/rogpeppe/go-internal v1.12.0 github.com/rs/cors v1.10.1 + github.com/rs/xid v1.5.0 github.com/stretchr/testify v1.9.0 github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 go.etcd.io/bbolt v1.3.9 @@ -45,10 +47,7 @@ require ( gopkg.in/yaml.v3 v3.0.1 ) -require github.com/rs/xid v1.5.0 // indirect - require ( - github.com/btcsuite/btcd/btcec/v2 v2.3.2 github.com/gdamore/encoding v1.0.0 // indirect github.com/go-chi/chi/v5 v5.0.12 // indirect github.com/golang/snappy v0.0.4 // indirect @@ -60,7 +59,7 @@ require ( github.com/rivo/uniseg v0.4.3 // indirect github.com/zondax/hid v0.9.2 // indirect github.com/zondax/ledger-go v0.14.3 // indirect - golang.org/x/sync v0.6.0 // indirect + golang.org/x/sync v0.7.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20231009173412-8bfb1ae86b6c // indirect diff --git a/go.sum b/go.sum index 9da3fc83d22..84b3e37993d 100644 --- a/go.sum +++ b/go.sum @@ -57,8 +57,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= -github.com/gnolang/faucet v0.1.3 h1:eg3S4rGkW6LYWo7nhc4AhWrPmHsaEFy6R8fyef/KgK4= -github.com/gnolang/faucet v0.1.3/go.mod h1:+91pqgE+pyX8FO9eoe2MGiwgTpYY0bITYsHO/4Zg+CY= +github.com/gnolang/faucet v0.2.0 h1:ynUgJ/z5F6to/4j5F9v7tpPth4b9ZUvLseGKd+tiL/g= +github.com/gnolang/faucet v0.2.0/go.mod h1:E7Nxsgmx4JbXWlBXE8teSj7IZf/BSwCfB2pO9hGY02E= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= @@ -199,8 +199,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/tm2/pkg/bft/rpc/client/client_test.go b/tm2/pkg/bft/rpc/client/client_test.go index b0ced270e64..06103803f04 100644 --- a/tm2/pkg/bft/rpc/client/client_test.go +++ b/tm2/pkg/bft/rpc/client/client_test.go @@ -120,6 +120,8 @@ func TestRPCClient_Status(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -152,6 +154,8 @@ func TestRPCClient_ABCIInfo(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -188,6 +192,8 @@ func TestRPCClient_ABCIQuery(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, path, params["path"]) assert.Equal(t, base64.StdEncoding.EncodeToString(data), params["data"]) assert.Equal(t, fmt.Sprintf("%d", opts.Height), params["height"]) @@ -223,6 +229,8 @@ func TestRPCClient_BroadcastTxCommit(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, base64.StdEncoding.EncodeToString(tx), params["tx"]) } @@ -255,6 +263,8 @@ func TestRPCClient_BroadcastTxAsync(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, base64.StdEncoding.EncodeToString(tx), params["tx"]) } @@ -287,6 +297,8 @@ func TestRPCClient_BroadcastTxSync(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, base64.StdEncoding.EncodeToString(tx), params["tx"]) } @@ -319,6 +331,8 @@ func TestRPCClient_UnconfirmedTxs(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, fmt.Sprintf("%d", limit), params["limit"]) } @@ -349,6 +363,8 @@ func TestRPCClient_NumUnconfirmedTxs(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -379,6 +395,8 @@ func TestRPCClient_NetInfo(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -411,6 +429,8 @@ func TestRPCClient_DumpConsensusState(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -443,6 +463,8 @@ func TestRPCClient_ConsensusState(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -475,6 +497,8 @@ func TestRPCClient_ConsensusParams(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, fmt.Sprintf("%d", blockHeight), params["height"]) } @@ -503,6 +527,8 @@ func TestRPCClient_Health(t *testing.T) { expectedResult = &ctypes.ResultHealth{} verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -536,6 +562,8 @@ func TestRPCClient_BlockchainInfo(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, fmt.Sprintf("%d", minHeight), params["minHeight"]) assert.Equal(t, fmt.Sprintf("%d", maxHeight), params["maxHeight"]) } @@ -569,6 +597,8 @@ func TestRPCClient_Genesis(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } @@ -605,6 +635,8 @@ func TestRPCClient_Block(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) } @@ -637,6 +669,8 @@ func TestRPCClient_BlockResults(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) } @@ -669,6 +703,8 @@ func TestRPCClient_Commit(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) } @@ -701,6 +737,8 @@ func TestRPCClient_Validators(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Equal(t, fmt.Sprintf("%d", height), params["height"]) } @@ -755,6 +793,8 @@ func TestRPCClient_Batch(t *testing.T) { } verifyFn = func(t *testing.T, params map[string]any) { + t.Helper() + assert.Len(t, params, 0) } diff --git a/tm2/pkg/crypto/keys/client/broadcast.go b/tm2/pkg/crypto/keys/client/broadcast.go index be034530816..423714b2141 100644 --- a/tm2/pkg/crypto/keys/client/broadcast.go +++ b/tm2/pkg/crypto/keys/client/broadcast.go @@ -100,7 +100,10 @@ func BroadcastHandler(cfg *BroadcastCfg) (*ctypes.ResultBroadcastTxCommit, error return nil, errors.Wrap(err, "remarshaling tx binary bytes") } - cli := client.NewHTTP(remote) + cli, err := client.NewHTTPClient(remote) + if err != nil { + return nil, err + } if cfg.DryRun { return SimulateTx(cli, bz) diff --git a/tm2/pkg/crypto/keys/client/query.go b/tm2/pkg/crypto/keys/client/query.go index 66e656e0758..e44bb796b9d 100644 --- a/tm2/pkg/crypto/keys/client/query.go +++ b/tm2/pkg/crypto/keys/client/query.go @@ -100,7 +100,11 @@ func QueryHandler(cfg *QueryCfg) (*ctypes.ResultABCIQuery, error) { // Height: height, XXX // Prove: false, XXX } - cli := client.NewHTTP(remote) + cli, err := client.NewHTTPClient(remote) + if err != nil { + return nil, errors.Wrap(err, "new http client") + } + qres, err := cli.ABCIQueryWithOptions( cfg.Path, data, opts2) if err != nil { From 8de8cf046e8e754e353b602378627a319b2d92a7 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Apr 2024 14:35:38 +0200 Subject: [PATCH 17/26] Move gnofaucet as a separate module --- README.md | 2 +- contribs/Makefile | 3 +- contribs/gnofaucet/go.mod | 39 ++++ contribs/gnofaucet/go.sum | 188 ++++++++++++++++++ {gno.land/cmd => contribs}/gnofaucet/main.go | 0 .../cmd => contribs}/gnofaucet/main_test.go | 0 .../cmd => contribs}/gnofaucet/middleware.go | 0 {gno.land/cmd => contribs}/gnofaucet/serve.go | 4 +- .../cmd => contribs}/gnofaucet/throttle.go | 0 .../gnofaucet/throttle_test.go | 0 gno.land/Makefile | 6 +- gno.land/README.md | 2 +- go.mod | 3 - go.sum | 6 - 14 files changed, 235 insertions(+), 18 deletions(-) create mode 100644 contribs/gnofaucet/go.mod create mode 100644 contribs/gnofaucet/go.sum rename {gno.land/cmd => contribs}/gnofaucet/main.go (100%) rename {gno.land/cmd => contribs}/gnofaucet/main_test.go (100%) rename {gno.land/cmd => contribs}/gnofaucet/middleware.go (100%) rename {gno.land/cmd => contribs}/gnofaucet/serve.go (97%) rename {gno.land/cmd => contribs}/gnofaucet/throttle.go (100%) rename {gno.land/cmd => contribs}/gnofaucet/throttle_test.go (100%) diff --git a/README.md b/README.md index 987a24f5b66..35acbee8e7a 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ repository offers more resources to dig into. We are eager to see your first PR! * [gno](./gnovm/cmd/gno) - handy tool for developing gno packages & realms * [goscan](./misc/goscan) - dumps imports from specified file’s AST * [genproto](./misc/genproto) - helper for generating .proto implementations - * [gnofaucet](./gno.land/cmd/gnofaucet) - serves GNOT faucet + * [gnofaucet](contribs/gnofaucet) - serves GNOT faucet
CI/CD/Tools badges and links diff --git a/contribs/Makefile b/contribs/Makefile index 94514809f27..9110a7e65f3 100644 --- a/contribs/Makefile +++ b/contribs/Makefile @@ -25,10 +25,11 @@ GOTEST_FLAGS ?= -v -p 1 -timeout=30m ######################################## # Dev tools .PHONY: install -install: install.gnomd install.gnodev +install: install.gnomd install.gnodev install.gnofaucet install.gnomd:; cd gnomd && go install . install.gnodev:; $(MAKE) -C ./gnodev install +install.gnofaucet:; cd gnofaucet && go install . .PHONY: clean clean: diff --git a/contribs/gnofaucet/go.mod b/contribs/gnofaucet/go.mod new file mode 100644 index 00000000000..3202b05aa30 --- /dev/null +++ b/contribs/gnofaucet/go.mod @@ -0,0 +1,39 @@ +module github.com/gnolang/gno/contribs/gnofaucet + +go 1.21 + +require ( + github.com/gnolang/faucet v0.2.0 + github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc + github.com/stretchr/testify v1.9.0 + go.uber.org/zap v1.27.0 + golang.org/x/time v0.5.0 +) + +require ( + github.com/btcsuite/btcd/btcec/v2 v2.3.3 // indirect + github.com/btcsuite/btcd/btcutil v1.1.5 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 // indirect + github.com/go-chi/chi/v5 v5.0.12 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/gorilla/websocket v1.5.1 // indirect + github.com/jaekwon/testify v1.6.1 // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/libp2p/go-buffer-pool v0.1.0 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.10.1 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.uber.org/zap/exp v0.2.0 // indirect + golang.org/x/crypto v0.21.0 // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/mod v0.16.0 // indirect + golang.org/x/net v0.23.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.18.0 // indirect + golang.org/x/term v0.18.0 // indirect + google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/contribs/gnofaucet/go.sum b/contribs/gnofaucet/go.sum new file mode 100644 index 00000000000..26e5d4b2fd0 --- /dev/null +++ b/contribs/gnofaucet/go.sum @@ -0,0 +1,188 @@ +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= +github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= +github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd h1:js1gPwhcFflTZ7Nzl7WHaOTlTr5hIrR4n1NM4v9n4Kw= +github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= +github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= +github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= +github.com/btcsuite/btcd/btcec/v2 v2.3.3 h1:6+iXlDKE8RMtKsvK0gshlXIuPbyWM/h84Ensb7o3sC0= +github.com/btcsuite/btcd/btcec/v2 v2.3.3/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= +github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= +github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= +github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= +github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= +github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/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/goleveldb v1.0.0/go.mod h1:QiK9vBlgftBg6rWQIj6wFzbPfRjiykIEhBH4obrXJ/I= +github.com/btcsuite/snappy-go v0.0.0-20151229074030-0bdef8d06723/go.mod h1:8woku9dyThutzjeg+3xrA5iCpBRH8XEEg3lh6TiUghc= +github.com/btcsuite/snappy-go v1.0.0/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/cockroachdb/apd/v3 v3.2.1 h1:U+8j7t0axsIgvQUqthuNm82HIrYXodOV2iWLWtEaIwg= +github.com/cockroachdb/apd/v3 v3.2.1/go.mod h1:klXJcjp+FffLTHlhIG69tezTDvdP065naDsHzKhYSqc= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +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/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= +github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= +github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= +github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/gnolang/faucet v0.2.0 h1:ynUgJ/z5F6to/4j5F9v7tpPth4b9ZUvLseGKd+tiL/g= +github.com/gnolang/faucet v0.2.0/go.mod h1:E7Nxsgmx4JbXWlBXE8teSj7IZf/BSwCfB2pO9hGY02E= +github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc h1:aYkkNfumtt9z8DeI7ZiFC+vMgFFadaGY0A97pXpOqZU= +github.com/gnolang/gno v0.0.0-20240313211052-3481a03c98bc/go.mod h1:jDARzJA+/H5YwCGpWuouqo4D0LMSNZVVgFQK/r/R7As= +github.com/gnolang/goleveldb v0.0.9 h1:Q7rGko9oXMKtQA+Apeeed5a3sjba/mcDhzJGoTVLCKE= +github.com/gnolang/goleveldb v0.0.9/go.mod h1:Dz6p9bmpy/FBESTgduiThZt5mToVDipcHGzj/zUOo8E= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= +github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= +github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= +github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= +github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= +github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jaekwon/testify v1.6.1 h1:4AtAJcR9GzXN5W4DdY7ie74iCPiJV1JJUJL90t2ZUyw= +github.com/jaekwon/testify v1.6.1/go.mod h1:Oun0RXIHI7osufabQ60i4Lqkj0GXLbqI1I7kgzBNm1U= +github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.4.0/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/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= +github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= +github.com/linxGnu/grocksdb v1.6.20 h1:C0SNv12/OBr/zOdGw6reXS+mKpIdQGb/AkZWjHYnO64= +github.com/linxGnu/grocksdb v1.6.20/go.mod h1:IbTMGpmWg/1pg2hcG9LlxkqyqiJymdCweaUrzsLRFmg= +github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= +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/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.14.0/go.mod h1:iSB4RoI2tjJc9BBv4NKIKWKya62Rps+oPG/Lv9klQyY= +github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= +github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= +github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA= +go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.uber.org/zap/exp v0.2.0 h1:FtGenNNeCATRB3CmB/yEUnjEFeJWpB/pMcy7e2bKPYs= +go.uber.org/zap/exp v0.2.0/go.mod h1:t0gqAIdh1MfKv9EwN/dLwfZnJxe9ITAZN78HEWPFWDQ= +golang.org/x/crypto v0.0.0-20170930174604-9419663f5a44/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic= +golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= +golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= +golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= +golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +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/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/gno.land/cmd/gnofaucet/main.go b/contribs/gnofaucet/main.go similarity index 100% rename from gno.land/cmd/gnofaucet/main.go rename to contribs/gnofaucet/main.go diff --git a/gno.land/cmd/gnofaucet/main_test.go b/contribs/gnofaucet/main_test.go similarity index 100% rename from gno.land/cmd/gnofaucet/main_test.go rename to contribs/gnofaucet/main_test.go diff --git a/gno.land/cmd/gnofaucet/middleware.go b/contribs/gnofaucet/middleware.go similarity index 100% rename from gno.land/cmd/gnofaucet/middleware.go rename to contribs/gnofaucet/middleware.go diff --git a/gno.land/cmd/gnofaucet/serve.go b/contribs/gnofaucet/serve.go similarity index 97% rename from gno.land/cmd/gnofaucet/serve.go rename to contribs/gnofaucet/serve.go index f4d59e9ce08..f1ca7536e71 100644 --- a/gno.land/cmd/gnofaucet/serve.go +++ b/contribs/gnofaucet/serve.go @@ -9,7 +9,7 @@ import ( "time" "github.com/gnolang/faucet" - tm2Client "github.com/gnolang/faucet/client/http" + faucetClient "github.com/gnolang/faucet/client/http" "github.com/gnolang/faucet/config" "github.com/gnolang/faucet/estimate/static" "github.com/gnolang/gno/gno.land/pkg/log" @@ -174,7 +174,7 @@ func execServe(ctx context.Context, cfg *serveCfg, io commands.IO) error { } // Create the client (HTTP) - cli := tm2Client.NewClient(cfg.remote) + cli := faucetClient.NewClient(cfg.remote) // Set up the logger logger := log.ZapLoggerToSlog( diff --git a/gno.land/cmd/gnofaucet/throttle.go b/contribs/gnofaucet/throttle.go similarity index 100% rename from gno.land/cmd/gnofaucet/throttle.go rename to contribs/gnofaucet/throttle.go diff --git a/gno.land/cmd/gnofaucet/throttle_test.go b/contribs/gnofaucet/throttle_test.go similarity index 100% rename from gno.land/cmd/gnofaucet/throttle_test.go rename to contribs/gnofaucet/throttle_test.go diff --git a/gno.land/Makefile b/gno.land/Makefile index ce19471ec50..eddab28270d 100644 --- a/gno.land/Makefile +++ b/gno.land/Makefile @@ -31,11 +31,10 @@ start.gnoland:; go run ./cmd/gnoland start start.gnoweb:; go run ./cmd/gnoweb .PHONY: build -build: build.gnoland build.gnokey build.gnoweb build.gnofaucet build.genesis +build: build.gnoland build.gnokey build.gnoweb build.genesis build.gnoland:; go build -o build/gnoland ./cmd/gnoland build.gnoweb:; go build -o build/gnoweb ./cmd/gnoweb -build.gnofaucet:; go build -o build/gnofaucet ./cmd/gnofaucet build.gnokey:; go build -o build/gnokey ./cmd/gnokey build.genesis:; go build -o build/genesis ./cmd/genesis @@ -43,11 +42,10 @@ run.gnoland:; go run ./cmd/gnoland start run.gnoweb:; go run ./cmd/gnoweb .PHONY: install -install: install.gnoland install.gnoweb install.gnofaucet install.gnokey install.genesis +install: install.gnoland install.gnoweb install.gnokey install.genesis install.gnoland:; go install ./cmd/gnoland install.gnoweb:; go install ./cmd/gnoweb -install.gnofaucet:; go install ./cmd/gnofaucet install.gnokey:; go install ./cmd/gnokey install.genesis:; go install ./cmd/genesis diff --git a/gno.land/README.md b/gno.land/README.md index 0598aa98c52..5fdd95a84ad 100644 --- a/gno.land/README.md +++ b/gno.land/README.md @@ -8,7 +8,7 @@ Use [`gnokey`](./cmd/gnokey) to interact with Gno.land's testnets, localnet, and For localnet setup, use [`gnoland`](./cmd/gnoland). -To add a web interface and faucet to your localnet, use [`gnoweb`](./cmd/gnoweb) and [`gnofaucet`](./cmd/gnofaucet). +To add a web interface and faucet to your localnet, use [`gnoweb`](./cmd/gnoweb) and [`gnofaucet`](../contribs/gnofaucet). ## Interchain diff --git a/go.mod b/go.mod index 6e689ccf874..ca3a4c7538e 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 github.com/fortytw2/leaktest v1.3.0 github.com/gdamore/tcell/v2 v2.7.4 - github.com/gnolang/faucet v0.2.0 github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 github.com/golang/protobuf v1.5.4 github.com/google/gofuzz v1.2.0 @@ -41,7 +40,6 @@ require ( golang.org/x/mod v0.16.0 golang.org/x/net v0.22.0 golang.org/x/term v0.18.0 - golang.org/x/time v0.5.0 golang.org/x/tools v0.19.0 google.golang.org/protobuf v1.33.0 gopkg.in/yaml.v3 v3.0.1 @@ -49,7 +47,6 @@ require ( require ( github.com/gdamore/encoding v1.0.0 // indirect - github.com/go-chi/chi/v5 v5.0.12 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect diff --git a/go.sum b/go.sum index 84b3e37993d..ea160ed6447 100644 --- a/go.sum +++ b/go.sum @@ -57,12 +57,8 @@ github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdk github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/tcell/v2 v2.7.4 h1:sg6/UnTM9jGpZU+oFYAsDahfchWAFW8Xx2yFinNSAYU= github.com/gdamore/tcell/v2 v2.7.4/go.mod h1:dSXtXTSK0VsW1biw65DZLZ2NKr7j0qP/0J7ONmsraWg= -github.com/gnolang/faucet v0.2.0 h1:ynUgJ/z5F6to/4j5F9v7tpPth4b9ZUvLseGKd+tiL/g= -github.com/gnolang/faucet v0.2.0/go.mod h1:E7Nxsgmx4JbXWlBXE8teSj7IZf/BSwCfB2pO9hGY02E= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216 h1:GKvsK3oLWG9B1GL7WP/VqwM6C92j5tIvB844oggL9Lk= github.com/gnolang/overflow v0.0.0-20170615021017-4d914c927216/go.mod h1:xJhtEL7ahjM1WJipt89gel8tHzfIl/LyMY+lCYh38d8= -github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s= -github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -232,8 +228,6 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= From 756e9c65090d05e7fcfd45e63ab11d779d6f69d8 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Apr 2024 14:51:47 +0200 Subject: [PATCH 18/26] Minor tidy --- tm2/pkg/bft/rpc/lib/server/handlers.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/server/handlers.go b/tm2/pkg/bft/rpc/lib/server/handlers.go index 0cb1c2ec88f..417f417ba26 100644 --- a/tm2/pkg/bft/rpc/lib/server/handlers.go +++ b/tm2/pkg/bft/rpc/lib/server/handlers.go @@ -653,11 +653,19 @@ func (wsc *wsConnection) readRoutine() { responses types.RPCResponses ) + // Try to unmarshal the requests as a batch if err := json.Unmarshal(in, &requests); err != nil { - // next, try to unmarshal as a single request + // Next, try to unmarshal as a single request var request types.RPCRequest if err := json.Unmarshal(in, &request); err != nil { - wsc.WriteRPCResponses(types.RPCResponses{types.RPCParseError(types.JSONRPCStringID(""), errors.Wrap(err, "error unmarshalling request"))}) + wsc.WriteRPCResponses( + types.RPCResponses{ + types.RPCParseError( + types.JSONRPCStringID(""), + errors.Wrap(err, "error unmarshalling request"), + ), + }, + ) return } @@ -671,7 +679,8 @@ func (wsc *wsConnection) readRoutine() { // 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("") { - wsc.Logger.Debug("WSJSONRPC received a notification, skipping... (please send a non-empty ID if you want to call a method)") + wsc.Logger.Debug("Skipping notification JSON-RPC request") + continue } From ac718e34b2e272337c41de3d337a2cb1cb165343 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Apr 2024 14:58:48 +0200 Subject: [PATCH 19/26] Update workflows and Dockerfile for gnofaucet --- .github/workflows/gnoland.yml | 1 - Dockerfile | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/gnoland.yml b/.github/workflows/gnoland.yml index 3ad778b79bb..0481be8ebb6 100644 --- a/.github/workflows/gnoland.yml +++ b/.github/workflows/gnoland.yml @@ -40,7 +40,6 @@ jobs: - gnoland - gnokey - gnoweb - - gnofaucet runs-on: ubuntu-latest timeout-minutes: 5 steps: diff --git a/Dockerfile b/Dockerfile index d55771e904d..2b69104a46d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ RUN go mod download ADD . ./ RUN go build -o ./build/gnoland ./gno.land/cmd/gnoland RUN go build -o ./build/gnokey ./gno.land/cmd/gnokey -RUN go build -o ./build/gnofaucet ./gno.land/cmd/gnofaucet +RUN go build -o ./build/gnofaucet ./contribs/gnofaucet RUN go build -o ./build/gnoweb ./gno.land/cmd/gnoweb RUN go build -o ./build/gno ./gnovm/cmd/gno RUN ls -la ./build From 1a05f590e5d222566dc8caa4cfb6f8a149d03b62 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Apr 2024 15:05:32 +0200 Subject: [PATCH 20/26] Update Dockerfile --- Dockerfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 2b69104a46d..44fe6dfb947 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,6 @@ RUN go mod download ADD . ./ RUN go build -o ./build/gnoland ./gno.land/cmd/gnoland RUN go build -o ./build/gnokey ./gno.land/cmd/gnokey -RUN go build -o ./build/gnofaucet ./contribs/gnofaucet RUN go build -o ./build/gnoweb ./gno.land/cmd/gnoweb RUN go build -o ./build/gno ./gnovm/cmd/gno RUN ls -la ./build From 4042c8f365528d0c82c38286d4f117bfc7bf5db3 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Tue, 16 Apr 2024 15:24:14 +0200 Subject: [PATCH 21/26] Remove gnofaucet from the Docker build workflow --- .github/workflows/docker.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0b4def650c0..a5d952d604a 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [gnoland-slim, gnokey-slim, gno-slim, gnofaucet-slim, gnoweb-slim] + target: [gnoland-slim, gnokey-slim, gno-slim, gnoweb-slim] steps: - name: Checkout uses: actions/checkout@v4 From 6602ba245920cf849244903e686bc7da2f8abb89 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 19 Apr 2024 11:23:50 +0200 Subject: [PATCH 22/26] Remove leftover Dockerfile changes --- .github/workflows/docker.yml | 6 +++--- contribs/gnofaucet/serve.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index a5d952d604a..9616f6c06dd 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -34,7 +34,7 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + - name: Build and push uses: docker/build-push-action@v3 with: @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - target: [gnoland-slim, gnokey-slim, gno-slim, gnoweb-slim] + target: [ gnoland-slim, gnokey-slim, gno-slim, gnofaucet-slim, gnoweb-slim ] steps: - name: Checkout uses: actions/checkout@v4 @@ -71,7 +71,7 @@ jobs: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - + - name: Build and push uses: docker/build-push-action@v3 with: diff --git a/contribs/gnofaucet/serve.go b/contribs/gnofaucet/serve.go index f1ca7536e71..f4d59e9ce08 100644 --- a/contribs/gnofaucet/serve.go +++ b/contribs/gnofaucet/serve.go @@ -9,7 +9,7 @@ import ( "time" "github.com/gnolang/faucet" - faucetClient "github.com/gnolang/faucet/client/http" + tm2Client "github.com/gnolang/faucet/client/http" "github.com/gnolang/faucet/config" "github.com/gnolang/faucet/estimate/static" "github.com/gnolang/gno/gno.land/pkg/log" @@ -174,7 +174,7 @@ func execServe(ctx context.Context, cfg *serveCfg, io commands.IO) error { } // Create the client (HTTP) - cli := faucetClient.NewClient(cfg.remote) + cli := tm2Client.NewClient(cfg.remote) // Set up the logger logger := log.ZapLoggerToSlog( From 84260a375b8f1da3229c50dea17a4d9587a6ebf4 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Apr 2024 14:34:15 +0200 Subject: [PATCH 23/26] Simplify parseRemoteAddr --- tm2/pkg/bft/rpc/lib/client/http/client.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/client/http/client.go b/tm2/pkg/bft/rpc/lib/client/http/client.go index d71cc62faf1..34d301deba2 100644 --- a/tm2/pkg/bft/rpc/lib/client/http/client.go +++ b/tm2/pkg/bft/rpc/lib/client/http/client.go @@ -230,16 +230,13 @@ func toClientAddress(remoteAddr string) (string, error) { func parseRemoteAddr(remoteAddr string) (network string, s string, err error) { parts := strings.SplitN(remoteAddr, "://", 2) var protocol, address string - switch { - case len(parts) == 1: + switch len(parts) { + case 1: // default to tcp if nothing specified protocol, address = protoTCP, remoteAddr - case len(parts) == 2: + case 2: protocol, address = parts[0], parts[1] - default: - return "", "", fmt.Errorf("invalid addr: %s", remoteAddr) } - return protocol, address, nil } From cff8577394929aea9d699c1c31de70f310a65dae Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Apr 2024 14:37:27 +0200 Subject: [PATCH 24/26] Move code out of the default switch --- tm2/pkg/bft/rpc/lib/client/ws/client.go | 99 +++++++++++++------------ 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client.go b/tm2/pkg/bft/rpc/lib/client/ws/client.go index 1254ca719f3..768d747557a 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client.go @@ -186,71 +186,72 @@ func (c *Client) runReadRoutine(ctx context.Context) { return default: - // Read the message from the active connection - _, data, err := c.conn.ReadMessage() - if err != nil { - if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { - c.logger.Error("failed to read response", "err", err) + } - return - } + // Read the message from the active connection + _, data, err := c.conn.ReadMessage() + if err != nil { + if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { + c.logger.Error("failed to read response", "err", err) - continue + return } - var ( - responses types.RPCResponses - responseHash string - ) - - // Try to unmarshal as a batch of responses first - if err := json.Unmarshal(data, &responses); err != nil { - // Try to unmarshal as a single response - var response types.RPCResponse - - if err := json.Unmarshal(data, &response); err != nil { - c.logger.Error("failed to parse response", "err", err, "data", string(data)) + continue + } - continue - } + var ( + responses types.RPCResponses + responseHash string + ) - // This is a single response, generate the unique ID - responseHash = generateIDHash(response.ID.String()) - responses = types.RPCResponses{response} - } else { - // This is a batch response, generate the unique ID - // from the combined IDs - ids := make([]string, 0, len(responses)) + // Try to unmarshal as a batch of responses first + if err := json.Unmarshal(data, &responses); err != nil { + // Try to unmarshal as a single response + var response types.RPCResponse - for _, response := range responses { - ids = append(ids, response.ID.String()) - } + if err := json.Unmarshal(data, &response); err != nil { + c.logger.Error("failed to parse response", "err", err, "data", string(data)) - responseHash = generateIDHash(ids...) + continue } - // Grab the response channel - c.requestMapMux.Lock() - ch := c.requestMap[responseHash] - if ch == nil { - c.requestMapMux.Unlock() - c.logger.Error("response listener not set", "hash", responseHash, "responses", responses) + // This is a single response, generate the unique ID + responseHash = generateIDHash(response.ID.String()) + responses = types.RPCResponses{response} + } else { + // This is a batch response, generate the unique ID + // from the combined IDs + ids := make([]string, 0, len(responses)) - continue + for _, response := range responses { + ids = append(ids, response.ID.String()) } - // Clear the entry for this ID - delete(c.requestMap, responseHash) + responseHash = generateIDHash(ids...) + } + + // Grab the response channel + c.requestMapMux.Lock() + ch := c.requestMap[responseHash] + if ch == nil { c.requestMapMux.Unlock() + c.logger.Error("response listener not set", "hash", responseHash, "responses", responses) + + continue + } - c.logger.Debug("received response", "hash", responseHash) + // Clear the entry for this ID + delete(c.requestMap, responseHash) + c.requestMapMux.Unlock() - // Alert the listener of the response - select { - case ch <- responses: - default: - c.logger.Warn("response listener timed out", "hash", responseHash) - } + c.logger.Debug("received response", "hash", responseHash) + + // Alert the listener of the response + select { + case ch <- responses: + default: + c.logger.Warn("response listener timed out", "hash", responseHash) } } } From d710fea0bd43ee7d14870e590bb27ad2d7287640 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Apr 2024 14:45:04 +0200 Subject: [PATCH 25/26] Handle internal context for request adding --- tm2/pkg/bft/rpc/lib/client/ws/client.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client.go b/tm2/pkg/bft/rpc/lib/client/ws/client.go index 768d747557a..df7fe5a6591 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client.go @@ -82,6 +82,8 @@ func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*ty select { case <-ctx.Done(): return nil, ErrTimedOut + case <-c.ctx.Done(): + return nil, ErrTimedOut case c.backlog <- request: } @@ -89,6 +91,8 @@ func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*ty select { case <-ctx.Done(): return nil, ErrTimedOut + case <-c.ctx.Done(): + return nil, ErrTimedOut case response := <-responseCh: // Make sure the ID matches if response[0].ID != request.ID { @@ -121,6 +125,8 @@ func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (typ select { case <-ctx.Done(): return nil, ErrTimedOut + case <-c.ctx.Done(): + return nil, ErrTimedOut case c.backlog <- requests: } @@ -128,6 +134,8 @@ func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (typ select { case <-ctx.Done(): return nil, ErrTimedOut + case <-c.ctx.Done(): + return nil, ErrTimedOut case responses := <-responseCh: // Make sure the length matches if len(responses) != len(requests) { From 133376edc3ae3a92a7d043dc80455054a50b9695 Mon Sep 17 00:00:00 2001 From: Milos Zivkovic Date: Fri, 26 Apr 2024 15:14:00 +0200 Subject: [PATCH 26/26] Utilize context causes for propagating errors --- tm2/pkg/bft/rpc/lib/client/ws/client.go | 31 ++++++++++++++++++------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/tm2/pkg/bft/rpc/lib/client/ws/client.go b/tm2/pkg/bft/rpc/lib/client/ws/client.go index df7fe5a6591..0b74cb7f5ce 100644 --- a/tm2/pkg/bft/rpc/lib/client/ws/client.go +++ b/tm2/pkg/bft/rpc/lib/client/ws/client.go @@ -24,8 +24,8 @@ type responseCh chan<- types.RPCResponses // Client is a WebSocket client implementation type Client struct { - ctx context.Context - cancelFn context.CancelFunc + ctx context.Context + cancelCauseFn context.CancelCauseFunc conn *websocket.Conn @@ -51,9 +51,9 @@ func NewClient(rpcURL string, opts ...Option) (*Client, error) { logger: log.NewNoopLogger(), } - ctx, cancelFn := context.WithCancel(context.Background()) + ctx, cancelFn := context.WithCancelCause(context.Background()) c.ctx = ctx - c.cancelFn = cancelFn + c.cancelCauseFn = cancelFn // Apply the options for _, opt := range opts { @@ -83,7 +83,7 @@ func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*ty case <-ctx.Done(): return nil, ErrTimedOut case <-c.ctx.Done(): - return nil, ErrTimedOut + return nil, context.Cause(c.ctx) case c.backlog <- request: } @@ -92,7 +92,7 @@ func (c *Client) SendRequest(ctx context.Context, request types.RPCRequest) (*ty case <-ctx.Done(): return nil, ErrTimedOut case <-c.ctx.Done(): - return nil, ErrTimedOut + return nil, context.Cause(c.ctx) case response := <-responseCh: // Make sure the ID matches if response[0].ID != request.ID { @@ -126,7 +126,7 @@ func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (typ case <-ctx.Done(): return nil, ErrTimedOut case <-c.ctx.Done(): - return nil, ErrTimedOut + return nil, context.Cause(c.ctx) case c.backlog <- requests: } @@ -135,7 +135,7 @@ func (c *Client) SendBatch(ctx context.Context, requests types.RPCRequests) (typ case <-ctx.Done(): return nil, ErrTimedOut case <-c.ctx.Done(): - return nil, ErrTimedOut + return nil, context.Cause(c.ctx) case responses := <-responseCh: // Make sure the length matches if len(responses) != len(requests) { @@ -202,6 +202,13 @@ func (c *Client) runReadRoutine(ctx context.Context) { if websocket.IsUnexpectedCloseError(err, websocket.CloseNormalClosure) { c.logger.Error("failed to read response", "err", err) + // Server dropped the connection, stop the client + if err = c.closeWithCause( + fmt.Errorf("server closed connection, %w", err), + ); err != nil { + c.logger.Error("unable to gracefully close client", "err", err) + } + return } @@ -266,7 +273,13 @@ func (c *Client) runReadRoutine(ctx context.Context) { // Close closes the WS client func (c *Client) Close() error { - c.cancelFn() + return c.closeWithCause(nil) +} + +// closeWithCause closes the client (and any open connection) +// with the given cause +func (c *Client) closeWithCause(err error) error { + c.cancelCauseFn(err) return c.conn.Close() }