diff --git a/tm2/pkg/bft/rpc/client/batch.go b/tm2/pkg/bft/rpc/client/batch.go index 9cee83b0f62..13eb1a4b067 100644 --- a/tm2/pkg/bft/rpc/client/batch.go +++ b/tm2/pkg/bft/rpc/client/batch.go @@ -142,16 +142,16 @@ func (b *RPCBatch) ABCIQuery(path string, data []byte) error { } func (b *RPCBatch) ABCIQueryWithOptions(path string, data []byte, opts ABCIQueryOptions) error { + params := map[string]any{ + "path": path, + "data": data, + "prove": opts.Prove, + } + if opts.Height > 0 { + params["height"] = opts.Height + } // Prepare the RPC request - request, err := newRequest( - abciQueryMethod, - map[string]any{ - "path": path, - "data": data, - "height": opts.Height, - "prove": opts.Prove, - }, - ) + request, err := newRequest(abciQueryMethod, params) if err != nil { return fmt.Errorf("unable to create request, %w", err) } diff --git a/tm2/pkg/bft/rpc/client/batch_test.go b/tm2/pkg/bft/rpc/client/batch_test.go index 52930e5c372..ef3b5c8087b 100644 --- a/tm2/pkg/bft/rpc/client/batch_test.go +++ b/tm2/pkg/bft/rpc/client/batch_test.go @@ -208,6 +208,24 @@ func TestRPCBatch_Endpoints(t *testing.T) { return castResult }, }, + { + abciQueryMethod, + &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte("dummy"), + Height: 10, + }, + }, + func(batch *RPCBatch) { + require.NoError(t, batch.ABCIQueryWithOptions("path", []byte("dummy"), ABCIQueryOptions{Height: 10, Prove: false})) + }, + func(result any) any { + castResult, ok := result.(*ctypes.ResultABCIQuery) + require.True(t, ok) + assert.Equal(t, int64(10), castResult.Response.Height) + return castResult + }, + }, { broadcastTxCommitMethod, &ctypes.ResultBroadcastTxCommit{ diff --git a/tm2/pkg/bft/rpc/client/client.go b/tm2/pkg/bft/rpc/client/client.go index e7c7d578ef3..68681cc702f 100644 --- a/tm2/pkg/bft/rpc/client/client.go +++ b/tm2/pkg/bft/rpc/client/client.go @@ -137,8 +137,8 @@ func (c *RPCClient) ABCIQueryWithOptions(path string, data []byte, opts ABCIQuer map[string]any{ "path": path, "data": data, - "height": opts.Height, "prove": opts.Prove, + "height": opts.Height, }, ) } diff --git a/tm2/pkg/bft/rpc/client/e2e_test.go b/tm2/pkg/bft/rpc/client/e2e_test.go index 08d4b9b735d..300beea6833 100644 --- a/tm2/pkg/bft/rpc/client/e2e_test.go +++ b/tm2/pkg/bft/rpc/client/e2e_test.go @@ -209,6 +209,22 @@ func TestRPCClient_E2E_Endpoints(t *testing.T) { assert.Equal(t, expectedResult, result) }, }, + { + abciQueryMethod, + &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Value: []byte("dummy"), + Height: 10, + }, + }, + func(client *RPCClient, expectedResult any) { + result, err := client.ABCIQueryWithOptions("path", []byte("dummy"), ABCIQueryOptions{Height: 10, Prove: false}) + require.NoError(t, err) + + assert.Equal(t, expectedResult, result) + assert.Equal(t, int64(10), result.Response.Height) + }, + }, { broadcastTxCommitMethod, &ctypes.ResultBroadcastTxCommit{ diff --git a/tm2/pkg/bft/rpc/client/types.go b/tm2/pkg/bft/rpc/client/types.go index 52427a1a818..90ed52c243c 100644 --- a/tm2/pkg/bft/rpc/client/types.go +++ b/tm2/pkg/bft/rpc/client/types.go @@ -42,7 +42,6 @@ type ABCIClient interface { 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) diff --git a/tm2/pkg/bft/rpc/core/abci.go b/tm2/pkg/bft/rpc/core/abci.go index aef90052f58..db0fb12dfad 100644 --- a/tm2/pkg/bft/rpc/core/abci.go +++ b/tm2/pkg/bft/rpc/core/abci.go @@ -1,15 +1,18 @@ package core import ( + "fmt" + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" ) -// Query the application for some information. +// Query the application for some information. It allows querying the +// application's state at a specific height. // // ```shell -// curl 'localhost:26657/abci_query?path=""&data="abcd"&prove=false' +// curl 'localhost:26657/abci_query?path=""&data="abcd"&height=10&prove=false' // ``` // // ```go @@ -21,7 +24,7 @@ import ( // } // // defer client.Stop() -// result, err := client.ABCIQuery("", "abcd", true) +// result, err := client.ABCIQuery("", "abcd", 10, true) // ``` // // > The above command returns JSON structured like this: @@ -33,7 +36,7 @@ import ( // "result": { // "response": { // "log": "exists", -// "height": "0", +// "height": "10", // "proof": "010114FED0DAD959F36091AD761C922ABA3CBF1D8349990101020103011406AA2262E2F448242DF2C2607C3CDC705313EE3B0001149D16177BC71E445476174622EA559715C293740C", // "value": "61626364", // "key": "61626364", @@ -56,6 +59,15 @@ import ( // | height | int64 | 0 | false | Height (0 means latest) | // | prove | bool | false | false | Includes proof if true | func ABCIQuery(ctx *rpctypes.Context, path string, data []byte, height int64, prove bool) (*ctypes.ResultABCIQuery, error) { + if height < 0 { + return nil, fmt.Errorf("height cannot be negative: %d", height) + } + currentHeight := blockStore.Height() + + if height > currentHeight { + return nil, fmt.Errorf("requested height %d is in the future (latest height is %d)", height, currentHeight) + } + resQuery, err := proxyAppQuery.QuerySync(abci.RequestQuery{ Path: path, Data: data, @@ -65,7 +77,7 @@ func ABCIQuery(ctx *rpctypes.Context, path string, data []byte, height int64, pr if err != nil { return nil, err } - logger.Info("ABCIQuery", "path", path, "data", data, "result", resQuery) + return &ctypes.ResultABCIQuery{Response: resQuery}, nil } diff --git a/tm2/pkg/bft/rpc/core/abci_test.go b/tm2/pkg/bft/rpc/core/abci_test.go new file mode 100644 index 00000000000..8460af8f0b7 --- /dev/null +++ b/tm2/pkg/bft/rpc/core/abci_test.go @@ -0,0 +1,101 @@ +package core + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + ctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/core/types" + rpctypes "github.com/gnolang/gno/tm2/pkg/bft/rpc/lib/types" +) + +func TestABCIQuery(t *testing.T) { + t.Parallel() + + origBlockStore, origProxyAppQuery := blockStore, proxyAppQuery + defer func() { + blockStore, proxyAppQuery = origBlockStore, origProxyAppQuery + }() + + tests := []struct { + name string + path string + data []byte + height int64 + prove bool + mockHeight int64 + mockQueryResp *abci.ResponseQuery + mockQueryErr error + expectedResult *ctypes.ResultABCIQuery + expectedError string + }{ + { + name: "valid query", + path: "/a/b/c", + data: []byte("data"), + height: 10, + prove: false, + mockHeight: 20, + mockQueryResp: &abci.ResponseQuery{ + Key: []byte("key"), + Value: []byte("result"), + Height: 10, + }, + expectedResult: &ctypes.ResultABCIQuery{ + Response: abci.ResponseQuery{ + Key: []byte("key"), + Value: []byte("result"), + Height: 10, + }, + }, + }, + { + name: "negative height", + height: -1, + mockHeight: 20, + expectedResult: nil, + expectedError: "height cannot be negative", + }, + { + name: "future height", + height: 30, + mockHeight: 20, + expectedResult: nil, + expectedError: "requested height 30 is in the future (latest height is 20)", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + mockBS := &mockBlockStore{ + heightFn: func() int64 { return tt.mockHeight }, + } + blockStore = mockBS + + mockPAQ := &mockProxyAppQuery{ + queryFn: func(req abci.RequestQuery) (abci.ResponseQuery, error) { + if tt.mockQueryResp != nil { + return *tt.mockQueryResp, tt.mockQueryErr + } + return abci.ResponseQuery{}, tt.mockQueryErr + }, + } + proxyAppQuery = mockPAQ + + result, err := ABCIQuery(&rpctypes.Context{}, tt.path, tt.data, tt.height, tt.prove) + + if tt.expectedError != "" { + assert.Error(t, err) + assert.Contains(t, err.Error(), tt.expectedError) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.Equal(t, tt.expectedResult, result) + } + }) + } +} diff --git a/tm2/pkg/bft/rpc/core/mock_test.go b/tm2/pkg/bft/rpc/core/mock_test.go index a6ffe948d00..67c4d90568a 100644 --- a/tm2/pkg/bft/rpc/core/mock_test.go +++ b/tm2/pkg/bft/rpc/core/mock_test.go @@ -1,6 +1,9 @@ package core -import "github.com/gnolang/gno/tm2/pkg/bft/types" +import ( + abci "github.com/gnolang/gno/tm2/pkg/bft/abci/types" + "github.com/gnolang/gno/tm2/pkg/bft/types" +) type ( heightDelegate func() int64 @@ -76,3 +79,26 @@ func (m *mockBlockStore) SaveBlock(block *types.Block, blockParts *types.PartSet m.saveBlockFn(block, blockParts, seenCommit) } } + +type mockProxyAppQuery struct { + queryFn func(req abci.RequestQuery) (abci.ResponseQuery, error) +} + +func (m *mockProxyAppQuery) QuerySync(req abci.RequestQuery) (abci.ResponseQuery, error) { + if m.queryFn != nil { + return m.queryFn(req) + } + return abci.ResponseQuery{}, nil +} + +func (m *mockProxyAppQuery) EchoSync(msg string) (abci.ResponseEcho, error) { + return abci.ResponseEcho{}, nil +} + +func (m *mockProxyAppQuery) InfoSync(req abci.RequestInfo) (abci.ResponseInfo, error) { + return abci.ResponseInfo{}, nil +} + +func (m *mockProxyAppQuery) Error() error { + return nil +} diff --git a/tm2/pkg/crypto/keys/client/query.go b/tm2/pkg/crypto/keys/client/query.go index 3278d0ea0ea..e274aa42a4f 100644 --- a/tm2/pkg/crypto/keys/client/query.go +++ b/tm2/pkg/crypto/keys/client/query.go @@ -13,8 +13,10 @@ import ( type QueryCfg struct { RootCfg *BaseCfg - Data string - Path string + Data string + Path string + Height int64 + Prove bool } func NewQueryCmd(rootCfg *BaseCfg, io commands.IO) *commands.Command { @@ -42,6 +44,18 @@ func (c *QueryCfg) RegisterFlags(fs *flag.FlagSet) { "", "query data bytes", ) + fs.Int64Var( + &c.Height, + "height", + 0, + "height to query (0 means latest)", + ) + fs.BoolVar( + &c.Prove, + "prove", + false, + "include proofs of the transactions inclusion in the block", + ) } func execQuery(cfg *QueryCfg, args []string, io commands.IO) error { @@ -69,6 +83,11 @@ func execQuery(cfg *QueryCfg, args []string, io commands.IO) error { io.Printf("height: %d\ndata: %s\n", height, string(resdata)) + if cfg.Prove { + io.Printf("proof: %x\n", + qres.Response.Proof, + ) + } return nil } @@ -80,8 +99,8 @@ func QueryHandler(cfg *QueryCfg) (*ctypes.ResultABCIQuery, error) { data := []byte(cfg.Data) opts2 := client.ABCIQueryOptions{ - // Height: height, XXX - // Prove: false, XXX + Height: cfg.Height, + Prove: cfg.Prove, } cli, err := client.NewHTTPClient(remote) if err != nil { diff --git a/tm2/pkg/sdk/baseapp.go b/tm2/pkg/sdk/baseapp.go index 0fa26b817e1..cfa86672aa2 100644 --- a/tm2/pkg/sdk/baseapp.go +++ b/tm2/pkg/sdk/baseapp.go @@ -389,10 +389,6 @@ func (app *BaseApp) Query(req abci.RequestQuery) (res abci.ResponseQuery) { default: return handleQueryCustom(app, path, req) } - - msg := "unknown query path " + req.Path - res.Error = ABCIError(std.ErrUnknownRequest(msg)) - return } func handleQueryApp(app *BaseApp, path []string, req abci.RequestQuery) (res abci.ResponseQuery) { @@ -474,6 +470,8 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res return } + state := app.CurrentState() + cacheMS, err := app.cms.MultiImmutableCacheWrapWithVersion(req.Height) if err != nil { res.Error = ABCIError(std.ErrInternal( @@ -487,7 +485,10 @@ func handleQueryCustom(app *BaseApp, path []string, req abci.RequestQuery) (res // cache wrap the commit-multistore for safety // XXX RunTxModeQuery? - ctx := NewContext(RunTxModeCheck, cacheMS, app.checkState.ctx.BlockHeader(), app.logger).WithMinGasPrices(app.minGasPrices) + ctx := state.ctx. + WithMultiStore(cacheMS). + WithMode(RunTxModeCheck). + WithMinGasPrices(app.minGasPrices) // Passes the query to the handler. res = handler.Query(ctx, req) @@ -933,9 +934,10 @@ func (app *BaseApp) Close() error { // ---------------------------------------------------------------------------- // State +// state represents the application state at a given point. type state struct { - ms store.MultiStore - ctx Context + ms store.MultiStore // The multi-store containing the application state + ctx Context // The context associated with the state } func (st *state) MultiCacheWrap() store.MultiStore { @@ -945,3 +947,18 @@ func (st *state) MultiCacheWrap() store.MultiStore { func (st *state) Context() Context { return st.ctx } + +// CurrentState returns the current state of the application. +// It returns the deliver state if it exists, otherwise it returns the check state. +func (app *BaseApp) CurrentState() state { + if app.deliverState != nil { + return state{ + ms: app.deliverState.ms, + ctx: app.checkState.ctx, + } + } + return state{ + ms: app.checkState.ms, + ctx: app.checkState.ctx, + } +}