diff --git a/app/config.go b/app/config.go index 1a6a707b0..6b20615e6 100644 --- a/app/config.go +++ b/app/config.go @@ -240,6 +240,7 @@ func UpdateConfig(datadir string) { GlobalConfig.PocketConfig.RPCTimeout = sdk.DefaultRPCTimeout GlobalConfig.PocketConfig.IavlCacheSize = sdk.DefaultIavlCacheSize GlobalConfig.PocketConfig.LeanPocket = sdk.DefaultLeanPocket + GlobalConfig.PocketConfig.ClientSessionSyncAllowance = sdk.DefaultSessionSyncAllowance // Backup and Save the File var jsonFile *os.File diff --git a/doc/guides/quickstart.md b/doc/guides/quickstart.md index d064dcb7c..8d829760b 100644 --- a/doc/guides/quickstart.md +++ b/doc/guides/quickstart.md @@ -250,6 +250,7 @@ Pocket Core provides a configuration file found in `/config/config.json - **"rpc_port"**: The port of Pocket Core's RPC - **"client_block_sync_allowance"**: The +/- allowance in blocks for a relay request \(security mechanism that can help filter misconfigured clients\) +- **"client_session_sync_allowance"**: The +/- allowance in amount of sessions for a relay request \(mechanism to allow for servicing stale sessions for whenever session height changes.) - **"max_evidence_cache_entries"**: Maximum number of relay evidence stored in cache memory - **"max_session_cache_entries"**: Maximum number of sessions stored in cache memory - **"json_sort_relay_responses"**: Detect and sort if relay response is in json \(can help response comparisons if diff --git a/types/config.go b/types/config.go index 83dde1dc9..2a66e1865 100644 --- a/types/config.go +++ b/types/config.go @@ -18,36 +18,37 @@ type SDKConfig struct { } type PocketConfig struct { - DataDir string `json:"data_dir"` - GenesisName string `json:"genesis_file"` - ChainsName string `json:"chains_name"` - EvidenceDBName string `json:"evidence_db_name"` - TendermintURI string `json:"tendermint_uri"` - KeybaseName string `json:"keybase_name"` - RPCPort string `json:"rpc_port"` - ClientBlockSyncAllowance int `json:"client_block_sync_allowance"` - MaxEvidenceCacheEntires int `json:"max_evidence_cache_entries"` - MaxSessionCacheEntries int `json:"max_session_cache_entries"` - JSONSortRelayResponses bool `json:"json_sort_relay_responses"` - RemoteCLIURL string `json:"remote_cli_url"` - UserAgent string `json:"user_agent"` - ValidatorCacheSize int64 `json:"validator_cache_size"` - ApplicationCacheSize int64 `json:"application_cache_size"` - RPCTimeout int64 `json:"rpc_timeout"` - PrometheusAddr string `json:"pocket_prometheus_port"` - PrometheusMaxOpenfiles int `json:"prometheus_max_open_files"` - MaxClaimAgeForProofRetry int `json:"max_claim_age_for_proof_retry"` - ProofPrevalidation bool `json:"proof_prevalidation"` - CtxCacheSize int `json:"ctx_cache_size"` - ABCILogging bool `json:"abci_logging"` - RelayErrors bool `json:"show_relay_errors"` - DisableTxEvents bool `json:"disable_tx_events"` - Cache bool `json:"-"` - IavlCacheSize int64 `json:"iavl_cache_size"` - ChainsHotReload bool `json:"chains_hot_reload"` - GenerateTokenOnStart bool `json:"generate_token_on_start"` - LeanPocket bool `json:"lean_pocket"` - LeanPocketUserKeyFileName string `json:"lean_pocket_user_key_file"` + DataDir string `json:"data_dir"` + GenesisName string `json:"genesis_file"` + ChainsName string `json:"chains_name"` + EvidenceDBName string `json:"evidence_db_name"` + TendermintURI string `json:"tendermint_uri"` + KeybaseName string `json:"keybase_name"` + RPCPort string `json:"rpc_port"` + ClientBlockSyncAllowance int `json:"client_block_sync_allowance"` + ClientSessionSyncAllowance int64 `json:"client_session_sync_allowance"` + MaxEvidenceCacheEntires int `json:"max_evidence_cache_entries"` + MaxSessionCacheEntries int `json:"max_session_cache_entries"` + JSONSortRelayResponses bool `json:"json_sort_relay_responses"` + RemoteCLIURL string `json:"remote_cli_url"` + UserAgent string `json:"user_agent"` + ValidatorCacheSize int64 `json:"validator_cache_size"` + ApplicationCacheSize int64 `json:"application_cache_size"` + RPCTimeout int64 `json:"rpc_timeout"` + PrometheusAddr string `json:"pocket_prometheus_port"` + PrometheusMaxOpenfiles int `json:"prometheus_max_open_files"` + MaxClaimAgeForProofRetry int `json:"max_claim_age_for_proof_retry"` + ProofPrevalidation bool `json:"proof_prevalidation"` + CtxCacheSize int `json:"ctx_cache_size"` + ABCILogging bool `json:"abci_logging"` + RelayErrors bool `json:"show_relay_errors"` + DisableTxEvents bool `json:"disable_tx_events"` + Cache bool `json:"-"` + IavlCacheSize int64 `json:"iavl_cache_size"` + ChainsHotReload bool `json:"chains_hot_reload"` + GenerateTokenOnStart bool `json:"generate_token_on_start"` + LeanPocket bool `json:"lean_pocket"` + LeanPocketUserKeyFileName string `json:"lean_pocket_user_key_file"` } func (c PocketConfig) GetLeanPocketUserKeyFilePath() string { @@ -82,6 +83,7 @@ const ( DefaultMaxEvidenceCacheEntries = 500 DefaultListenAddr = "tcp://0.0.0.0:" DefaultClientBlockSyncAllowance = 10 + DefaultSessionSyncAllowance = 1 // 1 session (irrespective of num blocks per session) DefaultJSONSortRelayResponses = true DefaultTxIndexer = "kv" DefaultRPCDisableTransactionEvents = true @@ -117,35 +119,36 @@ func DefaultConfig(dataDir string) Config { c := Config{ TendermintConfig: *config.DefaultConfig(), PocketConfig: PocketConfig{ - DataDir: dataDir, - GenesisName: DefaultGenesisName, - ChainsName: DefaultChainsName, - EvidenceDBName: DefaultEvidenceDBName, - TendermintURI: DefaultTMURI, - KeybaseName: DefaultKeybaseName, - RPCPort: DefaultRPCPort, - ClientBlockSyncAllowance: DefaultClientBlockSyncAllowance, - MaxEvidenceCacheEntires: DefaultMaxEvidenceCacheEntries, - MaxSessionCacheEntries: DefaultMaxSessionCacheEntries, - JSONSortRelayResponses: DefaultJSONSortRelayResponses, - RemoteCLIURL: DefaultRemoteCLIURL, - UserAgent: DefaultUserAgent, - ValidatorCacheSize: DefaultValidatorCacheSize, - ApplicationCacheSize: DefaultApplicationCacheSize, - RPCTimeout: DefaultRPCTimeout, - PrometheusAddr: DefaultPocketPrometheusListenAddr, - PrometheusMaxOpenfiles: DefaultPrometheusMaxOpenFile, - MaxClaimAgeForProofRetry: DefaultMaxClaimProofRetryAge, - ProofPrevalidation: DefaultProofPrevalidation, - CtxCacheSize: DefaultCtxCacheSize, - ABCILogging: DefaultABCILogging, - RelayErrors: DefaultRelayErrors, - DisableTxEvents: DefaultRPCDisableTransactionEvents, - IavlCacheSize: DefaultIavlCacheSize, - ChainsHotReload: DefaultChainHotReload, - GenerateTokenOnStart: DefaultGenerateTokenOnStart, - LeanPocket: DefaultLeanPocket, - LeanPocketUserKeyFileName: DefaultLeanPocketUserKeyFileName, + DataDir: dataDir, + GenesisName: DefaultGenesisName, + ChainsName: DefaultChainsName, + EvidenceDBName: DefaultEvidenceDBName, + TendermintURI: DefaultTMURI, + KeybaseName: DefaultKeybaseName, + RPCPort: DefaultRPCPort, + ClientBlockSyncAllowance: DefaultClientBlockSyncAllowance, + ClientSessionSyncAllowance: DefaultSessionSyncAllowance, + MaxEvidenceCacheEntires: DefaultMaxEvidenceCacheEntries, + MaxSessionCacheEntries: DefaultMaxSessionCacheEntries, + JSONSortRelayResponses: DefaultJSONSortRelayResponses, + RemoteCLIURL: DefaultRemoteCLIURL, + UserAgent: DefaultUserAgent, + ValidatorCacheSize: DefaultValidatorCacheSize, + ApplicationCacheSize: DefaultApplicationCacheSize, + RPCTimeout: DefaultRPCTimeout, + PrometheusAddr: DefaultPocketPrometheusListenAddr, + PrometheusMaxOpenfiles: DefaultPrometheusMaxOpenFile, + MaxClaimAgeForProofRetry: DefaultMaxClaimProofRetryAge, + ProofPrevalidation: DefaultProofPrevalidation, + CtxCacheSize: DefaultCtxCacheSize, + ABCILogging: DefaultABCILogging, + RelayErrors: DefaultRelayErrors, + DisableTxEvents: DefaultRPCDisableTransactionEvents, + IavlCacheSize: DefaultIavlCacheSize, + ChainsHotReload: DefaultChainHotReload, + GenerateTokenOnStart: DefaultGenerateTokenOnStart, + LeanPocket: DefaultLeanPocket, + LeanPocketUserKeyFileName: DefaultLeanPocketUserKeyFileName, }, } c.TendermintConfig.LevelDBOptions = config.DefaultLevelDBOpts() diff --git a/types/utils.go b/types/utils.go index 21c3d4cdb..971e6d306 100644 --- a/types/utils.go +++ b/types/utils.go @@ -34,6 +34,10 @@ func GetCacheKey(height int, value string) (key string) { return key } +func IsBetween(target, minInclusive, maxInclusive int64) bool { + return minInclusive <= target && target <= maxInclusive +} + // SortedJSON takes any JSON and returns it sorted by keys. Also, all white-spaces // are removed. // This method can be used to canonicalize JSON to be returned by GetSignBytes, diff --git a/x/pocketcore/keeper/service.go b/x/pocketcore/keeper/service.go index f2d437df8..6b0481edc 100644 --- a/x/pocketcore/keeper/service.go +++ b/x/pocketcore/keeper/service.go @@ -12,8 +12,14 @@ import ( // HandleRelay handles an api (read/write) request to a non-native (external) blockchain func (k Keeper) HandleRelay(ctx sdk.Ctx, relay pc.Relay) (*pc.RelayResponse, sdk.Error) { relayTimeStart := time.Now() - // get the latest session block height because this relay will correspond with the latest session - sessionBlockHeight := k.GetLatestSessionBlockHeight(ctx) + + sessionBlockHeight := relay.Proof.SessionBlockHeight + + if !k.IsProofSessionHeightWithinTolerance(ctx, sessionBlockHeight) { + // For legacy support, we are intentionally returning the invalid block height error. + return nil, pc.NewInvalidBlockHeightError(pc.ModuleName) + } + var node *pc.PocketNode // There is reference to node address so that way we don't have to recreate address twice for pre-leanpokt var nodeAddress sdk.Address diff --git a/x/pocketcore/keeper/service_test.go b/x/pocketcore/keeper/service_test.go index 5bfd61d9a..cd0766e92 100644 --- a/x/pocketcore/keeper/service_test.go +++ b/x/pocketcore/keeper/service_test.go @@ -119,8 +119,11 @@ func TestKeeper_HandleRelay(t *testing.T) { ctx, keeper, kvkeys, clientPrivateKey, appPrivateKey, nodePubKey, chain := setupHandleRelayTest(t) - SessionRangesToTest := 2 - // Eliminate the impact of ClientBlockSyncAllowance + // Store the original allowances to clean up at the end of this test + originalClientBlockSyncAllowance := types.GlobalPocketConfig.ClientBlockSyncAllowance + originalClientSessionSyncAllowance := types.GlobalPocketConfig.ClientSessionSyncAllowance + + // Eliminate the impact of ClientBlockSyncAllowance to avoid any relay meta validation errors (OutOfSyncError) types.GlobalPocketConfig.ClientBlockSyncAllowance = 10000 nodeBlockHeight := ctx.BlockHeight() @@ -128,6 +131,8 @@ func TestKeeper_HandleRelay(t *testing.T) { latestSessionHeight := keeper.GetLatestSessionBlockHeight(ctx) t.Cleanup(func() { + types.GlobalPocketConfig.ClientBlockSyncAllowance = originalClientBlockSyncAllowance + types.GlobalPocketConfig.ClientSessionSyncAllowance = originalClientSessionSyncAllowance gock.Off() // Flush pending mocks after test execution }) @@ -137,8 +142,11 @@ func TestKeeper_HandleRelay(t *testing.T) { mockCtx.On("BlockHeight").Return(ctx.BlockHeight()) mockCtx.On("Logger").Return(ctx.Logger()) mockCtx.On("PrevCtx", nodeBlockHeight).Return(ctx, nil) + + allSessionRangesTests := 4 // The range of block heights we will mock + // Set up mocks for heights we'll query later. - for i := int64(1); i <= blocksPerSesssion*int64(SessionRangesToTest); i++ { + for i := int64(1); i <= blocksPerSesssion*int64(allSessionRangesTests); i++ { mockCtx.On("PrevCtx", nodeBlockHeight-i).Return(ctx, nil) mockCtx.On("PrevCtx", nodeBlockHeight+i).Return(ctx, nil) } @@ -154,14 +162,16 @@ func TestKeeper_HandleRelay(t *testing.T) { nodePubKey, chain, ) - assert.Nil(t, err, err) + assert.Nil(t, err) assert.NotNil(t, resp) assert.NotEmpty(t, resp) assert.Equal(t, resp.Response, "bar") - // Client is behind or advanced beyond Node's height - // --> CodeInvalidBlockHeightError - for i := 1; i <= SessionRangesToTest; i++ { + // TC 1: + // Client is behind or advanced beyond Node's height with ClientSessionSyncAllowance 0 + // --> CodeInvalidBlockHeightError + types.GlobalPocketConfig.ClientSessionSyncAllowance = 0 + for i := 1; i <= allSessionRangesTests; i++ { resp, err = testRelayAt( t, mockCtx, @@ -191,4 +201,83 @@ func TestKeeper_HandleRelay(t *testing.T) { assert.Equal(t, err.Codespace(), sdk.CodespaceType(types.ModuleName)) assert.Equal(t, err.Code(), sdk.CodeType(types.CodeInvalidBlockHeightError)) } + + // TC2: + // Test a relay while one session behind and forward, + // while ClientSessionSyncAllowance = 1 + // --> Success on one session behind + // --> InvalidBlockHeightError on one session forward + sessionRangeTc := 1 + types.GlobalPocketConfig.ClientSessionSyncAllowance = int64(sessionRangeTc) + + // First test the minimum boundary + resp, err = testRelayAt( + t, + mockCtx, + keeper, + latestSessionHeight-blocksPerSesssion*int64(sessionRangeTc), + clientPrivateKey, + appPrivateKey, + nodePubKey, + chain, + ) + assert.Nil(t, err) + assert.NotNil(t, resp) + assert.NotEmpty(t, resp) + assert.Equal(t, resp.Response, "bar") + + // Second test the maximum boundary - Error + resp, err = testRelayAt( + t, + mockCtx, + keeper, + latestSessionHeight+blocksPerSesssion*int64(sessionRangeTc), + clientPrivateKey, + appPrivateKey, + nodePubKey, + chain, + ) + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, err.Codespace(), sdk.CodespaceType(types.ModuleName)) + assert.Equal(t, err.Code(), sdk.CodeType(types.CodeInvalidBlockHeightError)) + + // TC2: + // Test a relay while two sessions behind and forward, + // while ClientSessionSyncAllowance = 1 + // --> InvalidBlockHeightError on two sessions behind and forwards + sessionRangeTc = 2 + types.GlobalPocketConfig.ClientSessionSyncAllowance = 1 + + // First test two sessions back - Error + resp, err = testRelayAt( + t, + mockCtx, + keeper, + latestSessionHeight-blocksPerSesssion*int64(sessionRangeTc), + clientPrivateKey, + appPrivateKey, + nodePubKey, + chain, + ) + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, err.Codespace(), sdk.CodespaceType(types.ModuleName)) + assert.Equal(t, err.Code(), sdk.CodeType(types.CodeInvalidBlockHeightError)) + + // Second test two sessions forward - Error + resp, err = testRelayAt( + t, + mockCtx, + keeper, + latestSessionHeight+blocksPerSesssion*int64(sessionRangeTc), + clientPrivateKey, + appPrivateKey, + nodePubKey, + chain, + ) + assert.Nil(t, resp) + assert.NotNil(t, err) + assert.Equal(t, err.Codespace(), sdk.CodespaceType(types.ModuleName)) + assert.Equal(t, err.Code(), sdk.CodeType(types.CodeInvalidBlockHeightError)) } diff --git a/x/pocketcore/keeper/session.go b/x/pocketcore/keeper/session.go index 6466bbc39..9c546902b 100644 --- a/x/pocketcore/keeper/session.go +++ b/x/pocketcore/keeper/session.go @@ -55,6 +55,18 @@ func (k Keeper) IsSessionBlock(ctx sdk.Ctx) bool { return ctx.BlockHeight()%k.posKeeper.BlocksPerSession(ctx) == 1 } +// IsProofSessionHeightWithinTolerance checks if the relaySessionBlockHeight is bounded by (latestSessionBlockHeight - tolerance ) <= x <= latestSessionHeight +func (k Keeper) IsProofSessionHeightWithinTolerance(ctx sdk.Ctx, relaySessionBlockHeight int64) bool { + // Session block height can never be zero. + if relaySessionBlockHeight <= 0 { + return false + } + latestSessionHeight := k.GetLatestSessionBlockHeight(ctx) + tolerance := types.GlobalPocketConfig.ClientSessionSyncAllowance * k.posKeeper.BlocksPerSession(ctx) + minHeight := latestSessionHeight - tolerance + return sdk.IsBetween(relaySessionBlockHeight, minHeight, latestSessionHeight) +} + // "GetLatestSessionBlockHeight" - Returns the latest session block height (first block of the session, (see blocksPerSession)) func (k Keeper) GetLatestSessionBlockHeight(ctx sdk.Ctx) (sessionBlockHeight int64) { // get the latest block height