diff --git a/pkg/client/interface.go b/pkg/client/interface.go index fdbc3ef31..ab70b189e 100644 --- a/pkg/client/interface.go +++ b/pkg/client/interface.go @@ -1,3 +1,4 @@ +//go:generate mockgen -destination=../../testutil/mockclient/grpc_conn_mock.go -package=mockclient github.com/cosmos/gogoproto/grpc ClientConn //go:generate mockgen -destination=../../testutil/mockclient/events_query_client_mock.go -package=mockclient . Dialer,Connection,EventsQueryClient //go:generate mockgen -destination=../../testutil/mockclient/block_client_mock.go -package=mockclient . Block,BlockClient //go:generate mockgen -destination=../../testutil/mockclient/delegation_client_mock.go -package=mockclient . DelegationClient @@ -267,6 +268,8 @@ type AccountQueryClient interface { // ApplicationQueryClient defines an interface that enables the querying of the // on-chain application information type ApplicationQueryClient interface { + ParamsQuerier[*apptypes.Params] + // GetApplication queries the chain for the details of the application provided GetApplication(ctx context.Context, appAddress string) (apptypes.Application, error) @@ -299,8 +302,8 @@ type SessionQueryClient interface { // SharedQueryClient defines an interface that enables the querying of the // on-chain shared module params. type SharedQueryClient interface { - // GetParams queries the chain for the current shared module parameters. - GetParams(ctx context.Context) (*sharedtypes.Params, error) + ParamsQuerier[*sharedtypes.Params] + // GetSessionGracePeriodEndHeight returns the block height at which the grace period // for the session that includes queryHeight elapses. // The grace period is the number of blocks after the session ends during which relays @@ -333,6 +336,8 @@ type BlockQueryClient interface { // protobuf message. Since the generated go types don't include interface types, this // is necessary to prevent dependency cycles. type ProofParams interface { + cosmostypes.Msg + GetProofRequestProbability() float64 GetProofRequirementThreshold() *cosmostypes.Coin GetProofMissingPenalty() *cosmostypes.Coin @@ -342,13 +347,14 @@ type ProofParams interface { // ProofQueryClient defines an interface that enables the querying of the // on-chain proof module params. type ProofQueryClient interface { - // GetParams queries the chain for the current shared module parameters. - GetParams(ctx context.Context) (ProofParams, error) + ParamsQuerier[ProofParams] } // ServiceQueryClient defines an interface that enables the querying of the // on-chain service information type ServiceQueryClient interface { + ParamsQuerier[*servicetypes.Params] + // GetService queries the chain for the details of the service provided GetService(ctx context.Context, serviceId string) (sharedtypes.Service, error) GetServiceRelayDifficulty(ctx context.Context, serviceId string) (servicetypes.RelayMiningDifficulty, error) @@ -377,3 +383,18 @@ type HistoricalQueryCache[T any] interface { // SetAtHeight adds or updates a value at a specific height SetAtHeight(key string, value T, height int64) error } + +// ParamsQuerier represents a generic querier for module parameters. +// This interface should be implemented by any module-specific querier +// that needs to access and cache on-chain parameters. +// +// DEV_NOTE: Can't use cosmostypes.Msg instead of any because M +// would be a pointer but Keeper#GetParams() returns a value. 🙄 +type ParamsQuerier[P any] interface { + // GetParams queries the chain for the current module parameters, where + // P is the params type of a given module (e.g. sharedtypes.Params). + GetParams(ctx context.Context) (P, error) + // GetParamsAtHeight returns the parameters as they were at the specified + // height, where M is the params type of a given module (e.g. sharedtypes.Params). + GetParamsAtHeight(ctx context.Context, height int64) (P, error) +} diff --git a/pkg/client/query/appquerier.go b/pkg/client/query/appquerier.go index 9477c35f9..66d5eb41c 100644 --- a/pkg/client/query/appquerier.go +++ b/pkg/client/query/appquerier.go @@ -4,10 +4,11 @@ import ( "context" "cosmossdk.io/depinject" - grpc "github.com/cosmos/gogoproto/grpc" + gogogrpc "github.com/cosmos/gogoproto/grpc" "github.com/pokt-network/poktroll/pkg/client" apptypes "github.com/pokt-network/poktroll/x/application/types" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) var _ client.ApplicationQueryClient = (*appQuerier)(nil) @@ -16,7 +17,9 @@ var _ client.ApplicationQueryClient = (*appQuerier)(nil) // querying of on-chain application information through a single exposed method // which returns an apptypes.Application interface type appQuerier struct { - clientConn grpc.ClientConn + client.ParamsQuerier[*apptypes.Params] + + clientConn gogogrpc.ClientConn applicationQuerier apptypes.QueryClient } @@ -24,11 +27,30 @@ type appQuerier struct { // by injecting the dependecies provided by the depinject.Config // // Required dependencies: -// - clientCtx -func NewApplicationQuerier(deps depinject.Config) (client.ApplicationQueryClient, error) { - aq := &appQuerier{} +// - clientCtx (gogogrpc.ClientConn) +func NewApplicationQuerier( + deps depinject.Config, + opts ...ParamsQuerierOptionFn, +) (client.ApplicationQueryClient, error) { + cfg := DefaultParamsQuerierConfig() + for _, opt := range opts { + opt(cfg) + } + + paramsQuerier, err := NewCachedParamsQuerier[*apptypes.Params, apptypes.ApplicationQueryClient]( + deps, apptypes.NewAppQueryClient, + WithModuleInfo[*sharedtypes.Params](sharedtypes.ModuleName, sharedtypes.ErrSharedParamInvalid), + WithParamsCacheOptions(cfg.CacheOpts...), + ) + if err != nil { + return nil, err + } + + aq := &appQuerier{ + ParamsQuerier: paramsQuerier, + } - if err := depinject.Inject( + if err = depinject.Inject( deps, &aq.clientConn, ); err != nil { diff --git a/pkg/client/query/options.go b/pkg/client/query/options.go new file mode 100644 index 000000000..ee110d800 --- /dev/null +++ b/pkg/client/query/options.go @@ -0,0 +1,57 @@ +package query + +import ( + sdkerrors "cosmossdk.io/errors" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + + "github.com/pokt-network/poktroll/pkg/client/query/cache" +) + +// ParamsQuerierConfig holds the configuration for parameter queriers +type ParamsQuerierConfig struct { + // CacheOpts are the options passed to create the params cache + CacheOpts []cache.CacheOption + // ModuleName is used for logging and error context + ModuleName string + // ModuleParamError is the base error type for parameter query errors + ModuleParamError *sdkerrors.Error +} + +// ParamsQuerierOptionFn defines a function that configures a ParamsQuerierConfig +type ParamsQuerierOptionFn func(*ParamsQuerierConfig) + +// DefaultParamsQuerierConfig returns the default configuration for parameter queriers +func DefaultParamsQuerierConfig() *ParamsQuerierConfig { + return &ParamsQuerierConfig{ + CacheOpts: []cache.CacheOption{ + // TODO_IN_THIS_COMMIT: extract to constants. + cache.WithHistoricalMode(100), + // TODO_IN_THIS_COMMIT: reconcile the fact that MaxKeys doesn't apply to historical mode... + cache.WithMaxKeys(1), + // TODO_IN_THIS_COMMIT: extract to constants. + cache.WithEvictionPolicy(cache.FirstInFirstOut), + }, + } +} + +// WithModuleInfo sets the module-specific information for the querier +func WithModuleInfo[R cosmostypes.Msg](moduleName string, moduleParamError *sdkerrors.Error) ParamsQuerierOptionFn { + return func(cfg *ParamsQuerierConfig) { + cfg.ModuleName = moduleName + cfg.ModuleParamError = moduleParamError + } +} + +// WithParamsCacheOptions adds cache configuration options to the params querier +func WithParamsCacheOptions(opts ...cache.CacheOption) ParamsQuerierOptionFn { + return func(cfg *ParamsQuerierConfig) { + cfg.CacheOpts = append(cfg.CacheOpts, opts...) + } +} + +// WithCacheOptions adds cache configuration options to the shared querier +func WithCacheOptions(opts ...cache.CacheOption) ParamsQuerierOptionFn { + return func(cfg *ParamsQuerierConfig) { + cfg.CacheOpts = append(cfg.CacheOpts, opts...) + } +} diff --git a/pkg/client/query/paramsquerier.go b/pkg/client/query/paramsquerier.go new file mode 100644 index 000000000..58d80ebba --- /dev/null +++ b/pkg/client/query/paramsquerier.go @@ -0,0 +1,131 @@ +package query + +import ( + "context" + "errors" + + "cosmossdk.io/depinject" + cosmostypes "github.com/cosmos/cosmos-sdk/types" + gogogrpc "github.com/cosmos/gogoproto/grpc" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/query/cache" + "github.com/pokt-network/poktroll/pkg/polylog" +) + +var _ client.ParamsQuerier[cosmostypes.Msg] = (*cachedParamsQuerier[cosmostypes.Msg, paramsQuerierIface[cosmostypes.Msg]])(nil) + +// paramsQuerierIface is an interface which generated query clients MUST implement +// to be compatible with the cachedParamsQuerier. +// DEV_NOTE: It is mainly required due to syntactic constraints imposed by the generics +// (i.e. otherwise, P here MUST be a value type, and there's no way to express that Q +// (below) SHOULD be the concrete type of P in NewCachedParamsQuerier). +type paramsQuerierIface[P cosmostypes.Msg] interface { + GetParams(context.Context) (P, error) +} + +// NewCachedParamsQuerier creates a new params querier with the given configuration +func NewCachedParamsQuerier[P cosmostypes.Msg, Q paramsQuerierIface[P]]( + deps depinject.Config, + queryClientConstructor func(conn gogogrpc.ClientConn) Q, + opts ...ParamsQuerierOptionFn, +) (_ client.ParamsQuerier[P], err error) { + cfg := DefaultParamsQuerierConfig() + for _, opt := range opts { + opt(cfg) + } + + querier := &cachedParamsQuerier[P, Q]{ + config: cfg, + paramsCache: cache.NewInMemoryCache[P](cfg.CacheOpts...), + } + + if err = depinject.Inject( + deps, + &querier.clientConn, + ); err != nil { + return nil, err + } + + querier.queryClient = queryClientConstructor(querier.clientConn) + + return querier, nil +} + +// TODO_IN_THIS_COMMIT: update godoc... +// cachedParamsQuerier provides common functionality for all params queriers. +// It handles parameter caching and chain querying in a generic way, where +// R is the type of the parameters and Q is the type of the query client. +type cachedParamsQuerier[P cosmostypes.Msg, Q paramsQuerierIface[P]] struct { + clientConn gogogrpc.ClientConn + queryClient Q + paramsCache client.HistoricalQueryCache[P] + config *ParamsQuerierConfig +} + +// TODO_IN_THIS_COMMIT: update godoc... +// GetParams implements the common parameter querying with caching +func (bq *cachedParamsQuerier[P, Q]) GetParams(ctx context.Context) (P, error) { + logger := polylog.Ctx(ctx).With( + "querier", bq.config.ModuleName, + "method", "GetParams", + ) + + // Check cache first + var paramsZero P + cached, err := bq.paramsCache.Get("params") + switch { + case err == nil: + logger.Debug().Msg("cache hit") + return cached, nil + case !errors.Is(err, cache.ErrCacheMiss): + return paramsZero, err + } + + logger.Debug().Msg("cache miss") + + // Query chain on cache miss + params, err := bq.queryClient.GetParams(ctx) + if err != nil { + if bq.config.ModuleParamError != nil { + return paramsZero, bq.config.ModuleParamError.Wrap(err.Error()) + } + return paramsZero, err + } + + // Cache the result before returning + if err = bq.paramsCache.Set("params", params); err != nil { + return paramsZero, err + } + + return params, nil +} + +// TODO_IN_THIS_COMMIT: update godoc... +// GetParamsAtHeight returns parameters as they were at a specific height +func (bq *cachedParamsQuerier[P, Q]) GetParamsAtHeight(ctx context.Context, height int64) (P, error) { + logger := polylog.Ctx(ctx).With( + "querier", bq.config.ModuleName, + "method", "GetParamsAtHeight", + "height", height, + ) + + // Try to get from cache at specific height + cached, err := bq.paramsCache.GetAtHeight("params", height) + switch { + case err == nil: + logger.Debug().Msg("cache hit") + return cached, nil + case !errors.Is(err, cache.ErrCacheMiss): + return cached, err + } + + logger.Debug().Msg("cache miss") + + // TODO_MAINNET(@bryanchriswhite): Implement querying historical params from chain + err = cache.ErrCacheMiss.Wrapf("TODO: on-chain historical data not implemented") + logger.Error().Msgf("%s", err) + + // Meanwhile, return current params as fallback. 😬 + return bq.GetParams(ctx) +} diff --git a/pkg/client/query/proofquerier.go b/pkg/client/query/proofquerier.go index 30c2984cd..dd4a2218e 100644 --- a/pkg/client/query/proofquerier.go +++ b/pkg/client/query/proofquerier.go @@ -1,8 +1,6 @@ package query import ( - "context" - "cosmossdk.io/depinject" "github.com/cosmos/gogoproto/grpc" @@ -10,9 +8,17 @@ import ( prooftypes "github.com/pokt-network/poktroll/x/proof/types" ) +// TODO_IN_THIS_COMMIT: comment explaining why we can't use client.ProofQueryClient; +// tl;dr, it defines ian interface for ProofParams to avoid a dependency cycle +// (i.e. instead of importing prooftypes). +var _ client.ProofQueryClient = (*proofQuerier)(nil) + // proofQuerier is a wrapper around the prooftypes.QueryClient that enables the // querying of on-chain proof module params. type proofQuerier struct { + //client.ParamsQuerier[*prooftypes.Params] + client.ParamsQuerier[client.ProofParams] + clientConn grpc.ClientConn proofQuerier prooftypes.QueryClient } @@ -22,10 +28,33 @@ type proofQuerier struct { // // Required dependencies: // - grpc.ClientConn -func NewProofQuerier(deps depinject.Config) (client.ProofQueryClient, error) { - querier := &proofQuerier{} +func NewProofQuerier( + deps depinject.Config, + paramsQuerierOpts ...ParamsQuerierOptionFn, + // TODO_IN_THIS_COMMIT: comment explaining why we can't use client.ProofQueryClient; + // tl;dr, it defines ian interface for ProofParams to avoid a dependency cycle + // (i.e. instead of importing prooftypes). + // ) (paramsQuerierIface[*prooftypes.Params], error) { +) (paramsQuerierIface[client.ProofParams], error) { + paramsQuerierCfg := DefaultParamsQuerierConfig() + for _, opt := range paramsQuerierOpts { + opt(paramsQuerierCfg) + } + + paramsQuerier, err := NewCachedParamsQuerier[client.ProofParams, prooftypes.ProofQueryClient]( + deps, prooftypes.NewProofQueryClient, + WithModuleInfo[*prooftypes.Params](prooftypes.ModuleName, prooftypes.ErrProofParamInvalid), + WithParamsCacheOptions(paramsQuerierCfg.CacheOpts...), + ) + if err != nil { + return nil, err + } - if err := depinject.Inject( + querier := &proofQuerier{ + ParamsQuerier: paramsQuerier, + } + + if err = depinject.Inject( deps, &querier.clientConn, ); err != nil { @@ -36,15 +65,3 @@ func NewProofQuerier(deps depinject.Config) (client.ProofQueryClient, error) { return querier, nil } - -// GetParams queries the chain for the current proof module parameters. -func (pq *proofQuerier) GetParams( - ctx context.Context, -) (client.ProofParams, error) { - req := &prooftypes.QueryParamsRequest{} - res, err := pq.proofQuerier.Params(ctx, req) - if err != nil { - return nil, err - } - return &res.Params, nil -} diff --git a/pkg/client/query/servicequerier.go b/pkg/client/query/servicequerier.go index cb0629681..84beed5ad 100644 --- a/pkg/client/query/servicequerier.go +++ b/pkg/client/query/servicequerier.go @@ -4,7 +4,7 @@ import ( "context" "cosmossdk.io/depinject" - "github.com/cosmos/gogoproto/grpc" + gogogrpc "github.com/cosmos/gogoproto/grpc" "github.com/pokt-network/poktroll/pkg/client" servicetypes "github.com/pokt-network/poktroll/x/service/types" @@ -17,7 +17,9 @@ var _ client.ServiceQueryClient = (*serviceQuerier)(nil) // querying of on-chain service information through a single exposed method // which returns a sharedtypes.Service struct type serviceQuerier struct { - clientConn grpc.ClientConn + client.ParamsQuerier[*servicetypes.Params] + + clientConn gogogrpc.ClientConn serviceQuerier servicetypes.QueryClient } @@ -25,20 +27,39 @@ type serviceQuerier struct { // injecting the dependecies provided by the depinject.Config. // // Required dependencies: -// - clientCtx (grpc.ClientConn) -func NewServiceQuerier(deps depinject.Config) (client.ServiceQueryClient, error) { - servq := &serviceQuerier{} +// - clientCtx (gogogrpc.ClientConn) +func NewServiceQuerier( + deps depinject.Config, + paramsQuerierOpts ...ParamsQuerierOptionFn, +) (client.ServiceQueryClient, error) { + paramsQuerierCfg := DefaultParamsQuerierConfig() + for _, opt := range paramsQuerierOpts { + opt(paramsQuerierCfg) + } + + paramsQuerier, err := NewCachedParamsQuerier[*servicetypes.Params, servicetypes.ServiceQueryClient]( + deps, servicetypes.NewServiceQueryClient, + WithModuleInfo[*servicetypes.Params](servicetypes.ModuleName, servicetypes.ErrServiceParamInvalid), + WithParamsCacheOptions(paramsQuerierCfg.CacheOpts...), + ) + if err != nil { + return nil, err + } + + querier := &serviceQuerier{ + ParamsQuerier: paramsQuerier, + } - if err := depinject.Inject( + if err = depinject.Inject( deps, - &servq.clientConn, + &querier.clientConn, ); err != nil { return nil, err } - servq.serviceQuerier = servicetypes.NewQueryClient(servq.clientConn) + querier.serviceQuerier = servicetypes.NewQueryClient(querier.clientConn) - return servq, nil + return querier, nil } // GetService returns a sharedtypes.Service struct for a given serviceId. @@ -51,6 +72,8 @@ func (servq *serviceQuerier) GetService( Id: serviceId, } + // TODO_IN_THIS_COMMIT: historically cache services... + res, err := servq.serviceQuerier.Service(ctx, req) if err != nil { return sharedtypes.Service{}, ErrQueryRetrieveService.Wrapf( @@ -71,6 +94,8 @@ func (servq *serviceQuerier) GetServiceRelayDifficulty( ServiceId: serviceId, } + // TODO_IN_THIS_COMMIT: historically cache relay mining difficulties... + res, err := servq.serviceQuerier.RelayMiningDifficulty(ctx, req) if err != nil { return servicetypes.RelayMiningDifficulty{}, err diff --git a/pkg/client/query/sharedquerier.go b/pkg/client/query/sharedquerier.go index 06e0ed90a..1970778e2 100644 --- a/pkg/client/query/sharedquerier.go +++ b/pkg/client/query/sharedquerier.go @@ -13,48 +13,54 @@ import ( var _ client.SharedQueryClient = (*sharedQuerier)(nil) // sharedQuerier is a wrapper around the sharedtypes.QueryClient that enables the -// querying of on-chain shared information through a single exposed method -// which returns an sharedtypes.Session struct +// querying of on-chain shared information type sharedQuerier struct { + client.ParamsQuerier[*sharedtypes.Params] + clientConn grpc.ClientConn sharedQuerier sharedtypes.QueryClient blockQuerier client.BlockQueryClient } // NewSharedQuerier returns a new instance of a client.SharedQueryClient by -// injecting the dependecies provided by the depinject.Config. +// injecting the dependencies provided by the depinject.Config. // // Required dependencies: // - clientCtx (grpc.ClientConn) // - client.BlockQueryClient -func NewSharedQuerier(deps depinject.Config) (client.SharedQueryClient, error) { - querier := &sharedQuerier{} +func NewSharedQuerier( + deps depinject.Config, + paramsQuerierOpts ...ParamsQuerierOptionFn, +) (client.SharedQueryClient, error) { + paramsQuerierCfg := DefaultParamsQuerierConfig() + for _, opt := range paramsQuerierOpts { + opt(paramsQuerierCfg) + } + + paramsQuerier, err := NewCachedParamsQuerier[*sharedtypes.Params, sharedtypes.SharedQueryClient]( + deps, sharedtypes.NewSharedQueryClient, + WithModuleInfo[*sharedtypes.Params](sharedtypes.ModuleName, sharedtypes.ErrSharedParamInvalid), + WithParamsCacheOptions(paramsQuerierCfg.CacheOpts...), + ) + if err != nil { + return nil, err + } - if err := depinject.Inject( + sq := &sharedQuerier{ + ParamsQuerier: paramsQuerier, + } + + if err = depinject.Inject( deps, - &querier.clientConn, - &querier.blockQuerier, + &sq.clientConn, + &sq.blockQuerier, ); err != nil { return nil, err } - querier.sharedQuerier = sharedtypes.NewQueryClient(querier.clientConn) + sq.sharedQuerier = sharedtypes.NewQueryClient(sq.clientConn) - return querier, nil -} - -// GetParams queries & returns the shared module on-chain parameters. -// -// TODO_TECHDEBT(#543): We don't really want to have to query the params for every method call. -// Once `ModuleParamsClient` is implemented, use its replay observable's `#Last()` method -// to get the most recently (asynchronously) observed (and cached) value. -func (sq *sharedQuerier) GetParams(ctx context.Context) (*sharedtypes.Params, error) { - req := &sharedtypes.QueryParamsRequest{} - res, err := sq.sharedQuerier.Params(ctx, req) - if err != nil { - return nil, ErrQuerySessionParams.Wrapf("[%v]", err) - } - return &res.Params, nil + return sq, nil } // GetClaimWindowOpenHeight returns the block height at which the claim window of @@ -118,7 +124,11 @@ func (sq *sharedQuerier) GetSessionGracePeriodEndHeight( // to get the most recently (asynchronously) observed (and cached) value. // TODO_MAINNET(@bryanchriswhite, #543): We also don't really want to use the current value of the params. // Instead, we should be using the value that the params had for the session which includes queryHeight. -func (sq *sharedQuerier) GetEarliestSupplierClaimCommitHeight(ctx context.Context, queryHeight int64, supplierOperatorAddr string) (int64, error) { +func (sq *sharedQuerier) GetEarliestSupplierClaimCommitHeight( + ctx context.Context, + queryHeight int64, + supplierOperatorAddr string, +) (int64, error) { sharedParams, err := sq.GetParams(ctx) if err != nil { return 0, err @@ -151,7 +161,11 @@ func (sq *sharedQuerier) GetEarliestSupplierClaimCommitHeight(ctx context.Contex // to get the most recently (asynchronously) observed (and cached) value. // TODO_MAINNET(@bryanchriswhite, #543): We also don't really want to use the current value of the params. // Instead, we should be using the value that the params had for the session which includes queryHeight. -func (sq *sharedQuerier) GetEarliestSupplierProofCommitHeight(ctx context.Context, queryHeight int64, supplierOperatorAddr string) (int64, error) { +func (sq *sharedQuerier) GetEarliestSupplierProofCommitHeight( + ctx context.Context, + queryHeight int64, + supplierOperatorAddr string, +) (int64, error) { sharedParams, err := sq.GetParams(ctx) if err != nil { return 0, err diff --git a/pkg/client/query/sharedquerier_test.go b/pkg/client/query/sharedquerier_test.go new file mode 100644 index 000000000..bde227919 --- /dev/null +++ b/pkg/client/query/sharedquerier_test.go @@ -0,0 +1,122 @@ +package query_test + +import ( + "context" + "testing" + "time" + + "cosmossdk.io/depinject" + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "google.golang.org/grpc" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/query" + "github.com/pokt-network/poktroll/pkg/client/query/cache" + _ "github.com/pokt-network/poktroll/pkg/polylog/polyzero" + "github.com/pokt-network/poktroll/testutil/mockclient" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +type SharedQuerierTestSuite struct { + suite.Suite + ctrl *gomock.Controller + ctx context.Context + querier client.SharedQueryClient + mockConn *mockclient.MockClientConn + mockBlock *mockclient.MockCometRPC + TTL time.Duration +} + +func TestSharedQuerierSuite(t *testing.T) { + suite.Run(t, new(SharedQuerierTestSuite)) +} + +func (s *SharedQuerierTestSuite) SetupTest() { + s.ctrl = gomock.NewController(s.T()) + s.ctx = context.Background() + s.mockConn = mockclient.NewMockClientConn(s.ctrl) + s.mockBlock = mockclient.NewMockCometRPC(s.ctrl) + s.TTL = 200 * time.Millisecond + + deps := depinject.Supply(s.mockConn, s.mockBlock) + + // Create querier with test-specific cache settings + querier, err := query.NewSharedQuerier(deps, + query.WithCacheOptions( + cache.WithTTL(s.TTL), + cache.WithHistoricalMode(100), + ), + ) + require.NoError(s.T(), err) + require.NotNil(s.T(), querier) + + s.querier = querier +} + +func (s *SharedQuerierTestSuite) TearDownTest() { + s.ctrl.Finish() +} + +func (s *SharedQuerierTestSuite) TestRetrievesAndCachesParamsValues() { + multiplier := uint64(1000) + + // First query - params with multiplier 1000 + s.expectMockConnToReturnParamsWithMultiplierOnce(multiplier) + + // Initial query should fetch from chain. + params1, err := s.querier.GetParams(s.ctx) + s.NoError(err) + s.Equal(multiplier, params1.ComputeUnitsToTokensMultiplier) + + // Second query - should use cache, no mock expectation needed, this is + // asserted here due to the mock expectation calling Times(1). + params2, err := s.querier.GetParams(s.ctx) + s.NoError(err) + s.Equal(multiplier, params2.ComputeUnitsToTokensMultiplier) + + // Third query after 90% of the TTL - should still use cache. + time.Sleep(time.Duration(float64(s.TTL) * .9)) + params3, err := s.querier.GetParams(s.ctx) + s.NoError(err) + s.Equal(multiplier, params3.ComputeUnitsToTokensMultiplier) +} + +func (s *SharedQuerierTestSuite) TestHandlesCacheExpiration() { + // First query + s.expectMockConnToReturnParamsWithMultiplierOnce(2000) + + params1, err := s.querier.GetParams(s.ctx) + s.NoError(err) + s.Equal(uint64(2000), params1.ComputeUnitsToTokensMultiplier) + + // Wait for cache to expire + time.Sleep(300 * time.Millisecond) + + // Next query should hit the chain again + s.expectMockConnToReturnParamsWithMultiplierOnce(3000) + + params2, err := s.querier.GetParams(s.ctx) + s.NoError(err) + s.Equal(uint64(3000), params2.ComputeUnitsToTokensMultiplier) +} + +func (s *SharedQuerierTestSuite) expectMockConnToReturnParamsWithMultiplierOnce(multiplier uint64) { + s.mockConn.EXPECT(). + Invoke( + gomock.Any(), + "/poktroll.shared.Query/Params", + gomock.Any(), + gomock.Any(), + gomock.Any(), + ). + DoAndReturn(func(_ context.Context, _ string, _, reply any, _ ...grpc.CallOption) error { + resp := reply.(*sharedtypes.QueryParamsResponse) + params := sharedtypes.DefaultParams() + params.ComputeUnitsToTokensMultiplier = multiplier + + resp.Params = params + return nil + }).Times(1) +} diff --git a/x/application/types/expected_keepers.go b/x/application/types/expected_keepers.go index 3c908df63..55756ad3e 100644 --- a/x/application/types/expected_keepers.go +++ b/x/application/types/expected_keepers.go @@ -37,3 +37,7 @@ type SharedKeeper interface { GetParams(ctx context.Context) sharedtypes.Params GetSessionEndHeight(ctx context.Context, queryHeight int64) int64 } + +type ApplicationKeeper interface { + GetParams(ctx context.Context) Params +} diff --git a/x/application/types/query_client.go b/x/application/types/query_client.go new file mode 100644 index 000000000..682da5594 --- /dev/null +++ b/x/application/types/query_client.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + + gogogrpc "github.com/cosmos/gogoproto/grpc" +) + +// TODO_IN_THIS_COMMIT: godoc... +type ApplicationQueryClient interface { + QueryClient + + GetParams(context.Context) (*Params, error) +} + +// TODO_IN_THIS_COMMIT: godoc... +func NewAppQueryClient(conn gogogrpc.ClientConn) ApplicationQueryClient { + return NewQueryClient(conn).(ApplicationQueryClient) +} + +// TODO_IN_THIS_COMMIT: investigate generalization... +// TODO_IN_THIS_COMMIT: godoc... +func (c *queryClient) GetParams(ctx context.Context) (*Params, error) { + res, err := c.Params(ctx, &QueryParamsRequest{}) + if err != nil { + return nil, err + } + + params := res.GetParams() + return ¶ms, nil +} diff --git a/x/proof/types/application_query_client.go b/x/proof/types/application_query_client.go index 1cd887314..7c6d09997 100644 --- a/x/proof/types/application_query_client.go +++ b/x/proof/types/application_query_client.go @@ -5,6 +5,7 @@ import ( "github.com/pokt-network/poktroll/pkg/client" apptypes "github.com/pokt-network/poktroll/x/application/types" + sharedkeeper "github.com/pokt-network/poktroll/x/shared/keeper" ) var _ client.ApplicationQueryClient = (*AppKeeperQueryClient)(nil) @@ -13,6 +14,8 @@ var _ client.ApplicationQueryClient = (*AppKeeperQueryClient)(nil) // It does not rely on the QueryClient, and therefore does not make any // network requests as in the off-chain implementation. type AppKeeperQueryClient struct { + *sharedkeeper.KeeperParamsQuerier[apptypes.Params, ApplicationKeeper] + keeper ApplicationKeeper } @@ -22,7 +25,12 @@ type AppKeeperQueryClient struct { // has delegated its signing power to. // It should be injected into the RingClient when initialized from within the a keeper. func NewAppKeeperQueryClient(appKeeper ApplicationKeeper) client.ApplicationQueryClient { - return &AppKeeperQueryClient{keeper: appKeeper} + keeperParamsQuerier := sharedkeeper.NewKeeperParamsQuerier[apptypes.Params](appKeeper) + + return &AppKeeperQueryClient{ + keeper: appKeeper, + KeeperParamsQuerier: keeperParamsQuerier, + } } // GetApplication returns the application corresponding to the given address. diff --git a/x/proof/types/expected_keepers.go b/x/proof/types/expected_keepers.go index 9d1fd765e..22bfc9522 100644 --- a/x/proof/types/expected_keepers.go +++ b/x/proof/types/expected_keepers.go @@ -49,6 +49,7 @@ type ApplicationKeeper interface { GetApplication(ctx context.Context, address string) (app apptypes.Application, found bool) GetAllApplications(ctx context.Context) []apptypes.Application SetApplication(context.Context, apptypes.Application) + GetParams(ctx context.Context) apptypes.Params } // SharedKeeper defines the expected interface needed to retrieve shared information. diff --git a/x/proof/types/query_client.go b/x/proof/types/query_client.go new file mode 100644 index 000000000..de5e5e3b4 --- /dev/null +++ b/x/proof/types/query_client.go @@ -0,0 +1,33 @@ +package types + +import ( + "context" + + gogogrpc "github.com/cosmos/gogoproto/grpc" + + "github.com/pokt-network/poktroll/pkg/client" +) + +// TODO_IN_THIS_COMMIT: godoc... +type ProofQueryClient interface { + QueryClient + + GetParams(context.Context) (client.ProofParams, error) +} + +// TODO_IN_THIS_COMMIT: godoc... +func NewProofQueryClient(conn gogogrpc.ClientConn) ProofQueryClient { + return NewQueryClient(conn).(ProofQueryClient) +} + +// TODO_IN_THIS_COMMIT: investigate generalization... +// TODO_IN_THIS_COMMIT: godoc... +func (c *queryClient) GetParams(ctx context.Context) (client.ProofParams, error) { + res, err := c.Params(ctx, &QueryParamsRequest{}) + if err != nil { + return nil, err + } + + params := res.GetParams() + return ¶ms, nil +} diff --git a/x/proof/types/shared_query_client.go b/x/proof/types/shared_query_client.go index 574735e7e..9155b7cb7 100644 --- a/x/proof/types/shared_query_client.go +++ b/x/proof/types/shared_query_client.go @@ -4,6 +4,7 @@ import ( "context" "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/x/shared/keeper" sharedtypes "github.com/pokt-network/poktroll/x/shared/types" ) @@ -13,6 +14,8 @@ var _ client.SharedQueryClient = (*SharedKeeperQueryClient)(nil) // It does not rely on the QueryClient, and therefore does not make any // network requests as in the off-chain implementation. type SharedKeeperQueryClient struct { + *keeper.KeeperParamsQuerier[sharedtypes.Params, SharedKeeper] + sharedKeeper SharedKeeper sessionKeeper SessionKeeper } @@ -23,20 +26,15 @@ func NewSharedKeeperQueryClient( sharedKeeper SharedKeeper, sessionKeeper SessionKeeper, ) client.SharedQueryClient { + keeperParamsQuerier := keeper.NewKeeperParamsQuerier[sharedtypes.Params](sharedKeeper) + return &SharedKeeperQueryClient{ - sharedKeeper: sharedKeeper, - sessionKeeper: sessionKeeper, + KeeperParamsQuerier: keeperParamsQuerier, + sharedKeeper: sharedKeeper, + sessionKeeper: sessionKeeper, } } -// GetParams queries & returns the shared module on-chain parameters. -func (sqc *SharedKeeperQueryClient) GetParams( - ctx context.Context, -) (params *sharedtypes.Params, err error) { - sharedParams := sqc.sharedKeeper.GetParams(ctx) - return &sharedParams, nil -} - // GetSessionGracePeriodEndHeight returns the block height at which the grace period // for the session which includes queryHeight elapses. // The grace period is the number of blocks after the session ends during which relays @@ -48,8 +46,12 @@ func (sqc *SharedKeeperQueryClient) GetSessionGracePeriodEndHeight( ctx context.Context, queryHeight int64, ) (int64, error) { - sharedParams := sqc.sharedKeeper.GetParams(ctx) - return sharedtypes.GetSessionGracePeriodEndHeight(&sharedParams, queryHeight), nil + sharedParams, err := sqc.GetParamsAtHeight(ctx, queryHeight) + if err != nil { + return 0, err + } + + return sharedtypes.GetSessionGracePeriodEndHeight(sharedParams, queryHeight), nil } // GetClaimWindowOpenHeight returns the block height at which the claim window of @@ -61,8 +63,12 @@ func (sqc *SharedKeeperQueryClient) GetClaimWindowOpenHeight( ctx context.Context, queryHeight int64, ) (int64, error) { - sharedParams := sqc.sharedKeeper.GetParams(ctx) - return sharedtypes.GetClaimWindowOpenHeight(&sharedParams, queryHeight), nil + sharedParams, err := sqc.GetParamsAtHeight(ctx, queryHeight) + if err != nil { + return 0, err + } + + return sharedtypes.GetClaimWindowOpenHeight(sharedParams, queryHeight), nil } // GetProofWindowOpenHeight returns the block height at which the proof window of @@ -74,8 +80,12 @@ func (sqc *SharedKeeperQueryClient) GetProofWindowOpenHeight( ctx context.Context, queryHeight int64, ) (int64, error) { - sharedParams := sqc.sharedKeeper.GetParams(ctx) - return sharedtypes.GetProofWindowOpenHeight(&sharedParams, queryHeight), nil + sharedParams, err := sqc.GetParamsAtHeight(ctx, queryHeight) + if err != nil { + return 0, err + } + + return sharedtypes.GetProofWindowOpenHeight(sharedParams, queryHeight), nil } // GetEarliestSupplierClaimCommitHeight returns the earliest block height at which a claim @@ -109,8 +119,12 @@ func (sqc *SharedKeeperQueryClient) GetEarliestSupplierProofCommitHeight( queryHeight int64, supplierOperatorAddr string, ) (int64, error) { - sharedParams := sqc.sharedKeeper.GetParams(ctx) - proofWindowOpenHeight := sharedtypes.GetProofWindowOpenHeight(&sharedParams, queryHeight) + sharedParams, err := sqc.GetParamsAtHeight(ctx, queryHeight) + if err != nil { + return 0, err + } + + proofWindowOpenHeight := sharedtypes.GetProofWindowOpenHeight(sharedParams, queryHeight) // Fetch the proof window open block hash so that it can be used as part of the // pseudo-random seed for generating the proof distribution offset. @@ -119,7 +133,7 @@ func (sqc *SharedKeeperQueryClient) GetEarliestSupplierProofCommitHeight( // Get the earliest proof commit height for the given supplier. return sharedtypes.GetEarliestSupplierProofCommitHeight( - &sharedParams, + sharedParams, queryHeight, proofWindowOpenBlockHash, supplierOperatorAddr, @@ -133,6 +147,10 @@ func (sqc *SharedKeeperQueryClient) GetEarliestSupplierProofCommitHeight( // Since this will be a non-frequent occurrence, accounting for this edge case is // not an immediate blocker. func (sqc *SharedKeeperQueryClient) GetComputeUnitsToTokensMultiplier(ctx context.Context) (uint64, error) { - sharedParams := sqc.sharedKeeper.GetParams(ctx) + sharedParams, err := sqc.GetParamsAtHeight(ctx, 0) + if err != nil { + return 0, err + } + return sharedParams.GetComputeUnitsToTokensMultiplier(), nil } diff --git a/x/service/types/query_client.go b/x/service/types/query_client.go new file mode 100644 index 000000000..d321cee0c --- /dev/null +++ b/x/service/types/query_client.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + + gogogrpc "github.com/cosmos/gogoproto/grpc" +) + +// TODO_IN_THIS_COMMIT: godoc... +type ServiceQueryClient interface { + QueryClient + + GetParams(context.Context) (*Params, error) +} + +// TODO_IN_THIS_COMMIT: godoc... +func NewServiceQueryClient(conn gogogrpc.ClientConn) ServiceQueryClient { + return NewQueryClient(conn).(ServiceQueryClient) +} + +// TODO_IN_THIS_COMMIT: investigate generalization... +// TODO_IN_THIS_COMMIT: godoc... +func (c *queryClient) GetParams(ctx context.Context) (*Params, error) { + res, err := c.Params(ctx, &QueryParamsRequest{}) + if err != nil { + return nil, err + } + + params := res.GetParams() + return ¶ms, nil +} diff --git a/x/shared/keeper/params_query_client.go b/x/shared/keeper/params_query_client.go new file mode 100644 index 000000000..b4ded162c --- /dev/null +++ b/x/shared/keeper/params_query_client.go @@ -0,0 +1,83 @@ +package keeper + +import ( + "context" + "errors" + "fmt" + + "github.com/pokt-network/poktroll/pkg/client" + "github.com/pokt-network/poktroll/pkg/client/query/cache" + sharedtypes "github.com/pokt-network/poktroll/x/shared/types" +) + +var _ client.ParamsQuerier[*sharedtypes.Params] = (*KeeperParamsQuerier[sharedtypes.Params, Keeper])(nil) + +// DEV_NOTE: Can't use cosmostypes.Msg instead of any because M +// would be a pointer but GetParams() returns a value. 🙄 +type paramsKeeperIface[M any] interface { + GetParams(context.Context) M +} + +// KeeperParamsQuerier provides a base implementation of ParamsQuerier for keeper-based clients +type KeeperParamsQuerier[M any, K paramsKeeperIface[M]] struct { + keeper K + cache client.HistoricalQueryCache[M] +} + +// NewKeeperParamsQuerier creates a new KeeperParamsQuerier instance +func NewKeeperParamsQuerier[M any, K paramsKeeperIface[M]]( + keeper K, + opts ...cache.CacheOption, +) *KeeperParamsQuerier[M, K] { + // Use sensible defaults for keeper-based params cache + defaultOpts := []cache.CacheOption{ + cache.WithHistoricalMode(100), // Keep history of last 100 blocks + cache.WithEvictionPolicy(cache.FirstInFirstOut), + } + opts = append(defaultOpts, opts...) + + // TODO_IMPROVE: Implement and call a goroutine that subscribes to params updates to keep the cache hot. + + return &KeeperParamsQuerier[M, K]{ + keeper: keeper, + cache: cache.NewInMemoryCache[M](opts...), + } +} + +// GetParams retrieves current parameters from the keeper +func (kpq *KeeperParamsQuerier[M, K]) GetParams(ctx context.Context) (*M, error) { + // Check cache first + cached, err := kpq.cache.Get("params") + if err == nil { + return &cached, nil + } + if err != nil && !errors.Is(err, cache.ErrCacheMiss) { + return &cached, err + } + + // On cache miss, get from keeper + params := kpq.keeper.GetParams(ctx) + + // Cache the result + if err := kpq.cache.Set("params", params); err != nil { + return ¶ms, fmt.Errorf("failed to cache params: %w", err) + } + + return ¶ms, nil +} + +// GetParamsAtHeight retrieves parameters as they were at a specific height +func (kpq *KeeperParamsQuerier[M, K]) GetParamsAtHeight(ctx context.Context, height int64) (*M, error) { + // Try cache first + cached, err := kpq.cache.GetAtHeight("params", height) + if err == nil { + return &cached, nil + } + if err != nil && !errors.Is(err, cache.ErrCacheMiss) { + return &cached, err + } + + // For now, return current params as historical params are not yet implemented + // TODO_MAINNET: Implement historical parameter querying from state + return kpq.GetParams(ctx) +} diff --git a/x/shared/types/query_client.go b/x/shared/types/query_client.go new file mode 100644 index 000000000..443a374c6 --- /dev/null +++ b/x/shared/types/query_client.go @@ -0,0 +1,31 @@ +package types + +import ( + "context" + + gogogrpc "github.com/cosmos/gogoproto/grpc" +) + +// TODO_IN_THIS_COMMIT: godoc... +type SharedQueryClient interface { + QueryClient + + GetParams(context.Context) (*Params, error) +} + +// TODO_IN_THIS_COMMIT: godoc... +func NewSharedQueryClient(conn gogogrpc.ClientConn) SharedQueryClient { + return NewQueryClient(conn).(SharedQueryClient) +} + +// TODO_IN_THIS_COMMIT: investigate generalization... +// TODO_IN_THIS_COMMIT: godoc... +func (c *queryClient) GetParams(ctx context.Context) (*Params, error) { + res, err := c.Params(ctx, &QueryParamsRequest{}) + if err != nil { + return nil, err + } + + params := res.GetParams() + return ¶ms, nil +} diff --git a/x/tokenomics/types/expected_keepers.go b/x/tokenomics/types/expected_keepers.go index c7c0de0c5..0d864409d 100644 --- a/x/tokenomics/types/expected_keepers.go +++ b/x/tokenomics/types/expected_keepers.go @@ -46,6 +46,7 @@ type ApplicationKeeper interface { GetAllApplications(ctx context.Context) []apptypes.Application UnbondApplication(ctx context.Context, app *apptypes.Application) error EndBlockerUnbondApplications(ctx context.Context) error + GetParams(ctx context.Context) apptypes.Params } type ProofKeeper interface {