diff --git a/CHANGELOG.md b/CHANGELOG.md index b833829f..de95b4ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,18 @@ Contains bug fixes. Contains all the PRs that improved the code without changing the behaviors. --> +# v1.0.3-Prerelease + +## Added +- Added sentinel setup docs + +## Changed +- Updated sentinel to handle provider events + +## Fixed +- Fixed code lint +- Fixed ws client issue with event stream + # v1.0.2-Prerelease ## Added diff --git a/cmd/sentinel/main.go b/cmd/sentinel/main.go index d8cfb981..7e2e02d5 100644 --- a/cmd/sentinel/main.go +++ b/cmd/sentinel/main.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "github.com/arkeonetwork/arkeo/app" "github.com/arkeonetwork/arkeo/common/cosmos" "github.com/arkeonetwork/arkeo/sentinel" @@ -12,6 +14,10 @@ func main() { c.SetBech32PrefixForAccount(app.AccountAddressPrefix, app.AccountAddressPrefix+"pub") config := conf.NewConfiguration() - proxy := sentinel.NewProxy(config) + proxy, err := sentinel.NewProxy(config) + if err != nil { + fmt.Println(err) + return + } proxy.Run() } diff --git a/directory/utils/utils_test.go b/directory/utils/utils_test.go index befe79be..4479c766 100644 --- a/directory/utils/utils_test.go +++ b/directory/utils/utils_test.go @@ -96,11 +96,12 @@ func TestDownloadProviderMetadata(t *testing.T) { if err != nil { t.FailNow() } - + // nolint:staticcheck if metadata == nil { t.FailNow() } + // nolint:staticcheck if metadata.Version == "" { t.FailNow() } diff --git a/docs/SENTINEL.md b/docs/SENTINEL.md new file mode 100644 index 00000000..cb923669 --- /dev/null +++ b/docs/SENTINEL.md @@ -0,0 +1,133 @@ +# ๐Ÿ› ๏ธ Setting Up Sentinel + +## ๐ŸŒŸ Becoming a Provider + +### ๐Ÿช™ Create a Wallet Account for the Provider + +To create a wallet account for the provider, run the following command: + +```shell +arkeod add --keyring-backend test +``` + +### ๐Ÿ” Get the Provider Public Key + +Retrieve the provider's public key with: + +```bash +arkeod show -p --keyring-backend test | jq -r .key +``` + +Convert the result to Bech32 format: + +```bash +arkeod debug pubkey-raw | grep "Bech32 Acc" | awk '{ print $NF }' +``` + +> **โ„น๏ธ Note:** Request tokens from the faucet to bond the provider in the relevant ๐Ÿ’ฌ Discord channel. + +### ๐Ÿค Bond the Provider + +Bond your provider by executing the following command: + +```shell +arkeod tx arkeo bond-provider --from --keyring-backend ๐Ÿงช --fees 20uarkeo +``` + +## ๐Ÿš€ Starting the Sentinel Service + +### ๐Ÿ› ๏ธ Build the Sentinel Binary + +Compile the Sentinel binary by running: + +```bash +TAG=testnet make install +``` + +### โš™๏ธ Set Environment Variables + +Configure the environment variables as follows: + +```bash +NET="testnet" \ +MONIKER="" \ +WEBSITE="" \ +DESCRIPTION="" \ +LOCATION="" \ +PORT="" \ +SOURCE_CHAIN="" \ +EVENT_STREAM_HOST="" \ +FREE_RATE_LIMIT= \ +FREE_RATE_LIMIT_DURATION="" \ +CLAIM_STORE_LOCATION="~/.arkeo/claims" \ +CONTRACT_CONFIG_STORE_LOCATION="~/.arkeo/contract_configs" \ +PROVIDER_PUBKEY="" \ +PROVIDER_CONFIG_STORE_LOCATION="~/.arkeo/provider" +``` + +### โ–ถ๏ธ Run Sentinel + +Start the Sentinel service by executing: + +```bash +sentinel +``` + +When Sentinel starts, you should see output similar to the following: + +```bash +I[2024-10-28|11:58:20.056] Starting Sentinel (reverse proxy).... +Moniker +Website +Description +Location +Port +TLS Certificate +TLS Key +Source Chain +Event Stream Host +Provider PubKey +Claim Store Location ~/.arkeo/claims +Contract Config Store Location ~/.arkeo/contract_configs +Free Tier Rate Limit requests per +Provider Config Store Location ~/.arkeo/provider +I[2024-10-28|11:58:20.057] service start msg="Starting WSEvents service" impl=WSEvents +``` + +## ๐Ÿ“ Add Provider Metadata + +Once the Sentinel service is running, update the provider metadata by running: + +```shell +arkeod tx arkeo mod-provider "http:///metadata.json" --from --keyring-backend --fees 20uarkeo +``` + +## Sequence Diagram + +```mermaid +sequenceDiagram + participant Provider + participant ARKEO_CLI + participant ARKEO + participant Sentinel + + Provider->>ARKEO_CLI: Bond Provider (with bond amount and service) + ARKEO_CLI->>ARKEO: Validate and Execute Transaction + ARKEO-->>ARKEO_CLI: Tx hash returned to user + ARKEO_CLI-->>Provider: Verify Tx hash for success + ARKEO-->>ARKEO_CLI: Tx Result + ARKEO_CLI-->>Provider: Tx Result + + Provider->>Sentinel: Starts Sentinel with Provider PubKey + Sentinel-->>Provider: URL for provider metadata and service data + Sentinel->>ARKEO: Listen to Events via Websockets + ARKEO->>Sentinel: Websocket Response + + Provider->>ARKEO_CLI: Create Tx Mod Provider (with Sentinel Address) + ARKEO_CLI->>ARKEO: Validate and Execute Transaction + ARKEO-->>ARKEO_CLI: Tx hash + ARKEO-->>ARKEO_CLI: Tx Result + ARKEO_CLI-->>Provider: Verify Tx hash for success + ARKEO_CLI-->>Provider: Tx Result + +``` \ No newline at end of file diff --git a/readme.md b/readme.md index cf7b78a2..c79c015e 100644 --- a/readme.md +++ b/readme.md @@ -63,10 +63,10 @@ ignite chain serve ## Local -Build Binary +Installing Arkeo Binary ```shell -make proto-gen install +make install ``` Run diff --git a/sentinel/auth.go b/sentinel/auth.go index caaf56cd..e002ab10 100644 --- a/sentinel/auth.go +++ b/sentinel/auth.go @@ -224,7 +224,7 @@ func (p Proxy) auth(next http.Handler) http.Handler { if conf.PerUserRateLimit > 0 { if ok := p.isRateLimited(contract.Id, remoteAddr, conf.PerUserRateLimit); ok { - http.Error(w, http.StatusText(429), http.StatusTooManyRequests) + http.Error(w, "Too Many Requests", http.StatusTooManyRequests) return } } diff --git a/sentinel/auth_test.go b/sentinel/auth_test.go index ecf82f38..533c7475 100644 --- a/sentinel/auth_test.go +++ b/sentinel/auth_test.go @@ -64,7 +64,8 @@ func TestFreeTier(t *testing.T) { config := conf.Configuration{ FreeTierRateLimit: 1, } - proxy := NewProxy(config) + proxy, err := NewProxy(config) + require.NoError(t, err) remoteAddr := "127.0.0.1:8000" @@ -107,7 +108,8 @@ func TestPaidTier(t *testing.T) { ProviderPubKey: pubkey, FreeTierRateLimit: 1, } - proxy := NewProxy(config) + proxy, err := NewProxy(config) + require.NoError(t, err) contract := types.NewContract(pubkey, common.BTCService, pk) contract.Height = 5 diff --git a/sentinel/conf/configuration.go b/sentinel/conf/configuration.go index 1fa6edbf..6f9b6b14 100644 --- a/sentinel/conf/configuration.go +++ b/sentinel/conf/configuration.go @@ -21,10 +21,11 @@ type Configuration struct { Description string `json:"description"` Location string `json:"location"` Port string `json:"port"` - SourceChain string `json:"source_chain"` // base url for arceo block chain + SourceChain string `json:"source_chain"` // base url for arkeo block chain EventStreamHost string `json:"event_stream_host"` ClaimStoreLocation string `json:"claim_store_location"` // file location where claims are stored ContractConfigStoreLocation string `json:"contract_config_store_location"` // file location where contract configurations are stored + ProviderConfigStoreLocation string `json:"provider_config_store_location"` // file location where provider configurations are stored ProviderPubKey common.PubKey `json:"provider_pubkey"` FreeTierRateLimit int `json:"free_tier_rate_limit"` TLS TLSConfiguration `json:"tls"` @@ -95,6 +96,7 @@ func NewConfiguration() Configuration { ClaimStoreLocation: loadVarString("CLAIM_STORE_LOCATION"), ContractConfigStoreLocation: loadVarString("CONTRACT_CONFIG_STORE_LOCATION"), TLS: NewTLSConfiguration(), + ProviderConfigStoreLocation: loadVarString("PROVIDER_CONFIG_STORE_LOCATION"), } } @@ -113,5 +115,6 @@ func (c Configuration) Print() { fmt.Fprintln(writer, "Claim Store Location\t", c.ClaimStoreLocation) fmt.Fprintln(writer, "Contract Config Store Location\t", c.ContractConfigStoreLocation) fmt.Fprintln(writer, "Free Tier Rate Limit\t", fmt.Sprintf("%d requests per 1m", c.FreeTierRateLimit)) + fmt.Fprintln(writer, "Provider Config Store Location\t", c.ProviderConfigStoreLocation) writer.Flush() } diff --git a/sentinel/conf/configuration_test.go b/sentinel/conf/configuration_test.go index dc4687bc..7aa8b998 100644 --- a/sentinel/conf/configuration_test.go +++ b/sentinel/conf/configuration_test.go @@ -19,6 +19,7 @@ func TestConfiguration(t *testing.T) { os.Setenv("FREE_RATE_LIMIT", "99") os.Setenv("CLAIM_STORE_LOCATION", "clammy") os.Setenv("CONTRACT_CONFIG_STORE_LOCATION", "configy") + os.Setenv("PROVIDER_CONFIG_STORE_LOCATION", "providy") config := NewConfiguration() @@ -32,4 +33,5 @@ func TestConfiguration(t *testing.T) { require.Equal(t, config.FreeTierRateLimit, 99) require.Equal(t, config.ClaimStoreLocation, "clammy") require.Equal(t, config.ContractConfigStoreLocation, "configy") + require.Equal(t, config.ProviderConfigStoreLocation, "providy") } diff --git a/sentinel/event_stream.go b/sentinel/event_stream.go index 8f5fd193..55da09aa 100644 --- a/sentinel/event_stream.go +++ b/sentinel/event_stream.go @@ -8,12 +8,15 @@ import ( "strings" "syscall" + "cosmossdk.io/errors" "github.com/gogo/protobuf/proto" "github.com/arkeonetwork/arkeo/common" + "github.com/arkeonetwork/arkeo/common/cosmos" "github.com/cometbft/cometbft/libs/log" + tmlog "github.com/cometbft/cometbft/libs/log" tmclient "github.com/cometbft/cometbft/rpc/client/http" tmCoreTypes "github.com/cometbft/cometbft/rpc/core/types" tmtypes "github.com/cometbft/cometbft/types" @@ -22,6 +25,8 @@ import ( "github.com/arkeonetwork/arkeo/x/arkeo/types" ) +var numOfWebSocketClients = 2 + func subscribe(client *tmclient.HTTP, logger log.Logger, query string) <-chan tmCoreTypes.ResultEvent { out, err := client.Subscribe(context.Background(), "", query) if err != nil { @@ -31,26 +36,46 @@ func subscribe(client *tmclient.HTTP, logger log.Logger, query string) <-chan tm return out } -func (p Proxy) EventListener(host string) { - logger := p.logger - client, err := tmclient.New(fmt.Sprintf("tcp://%s", host), "/websocket") +func NewTendermintClient(baseURL string) (*tmclient.HTTP, error) { + client, err := tmclient.New(baseURL, "/websocket") if err != nil { - logger.Error("failure to create websocket client", "error", err) - panic(err) + return nil, errors.Wrapf(err, "error creating websocket client") } + logger := tmlog.NewTMLogger(tmlog.NewSyncWriter(os.Stdout)) client.SetLogger(logger) - err = client.Start() - if err != nil { - logger.Error("Failed to start a client", "err", err) - os.Exit(1) - } - defer client.Stop() // nolint - // create a unified channel for receiving events + return client, nil +} + +func (p Proxy) EventListener(host string) { + logger := p.logger + + logger.Info("starting realtime indexing using /websocket") + + // as maximum allowed connection is 5 per ws client(cometbft) we split this into 2 client to handle 3 connection each + clients := make([]*tmclient.HTTP, numOfWebSocketClients) + for i := 0; i < numOfWebSocketClients; i++ { + client, err := NewTendermintClient(fmt.Sprintf("tcp://%s", host)) + if err != nil { + panic(fmt.Sprintf("error creating tm client for %s: %+v", host, err)) + } + if err = client.Start(); err != nil { + panic(fmt.Sprintf("error starting ws client: %s: %+v", host, err)) + } + defer func() { + if err := client.Stop(); err != nil { + logger.Error("Failed to stop the client", "error", err) + } + }() + clients[i] = client + } + + // Create a unified channel for receiving events eventChan := make(chan tmCoreTypes.ResultEvent, 1000) - subscribeToEvents := func(queries ...string) { + // Function to subscribe to events for a given client + subscribeToEvents := func(client *tmclient.HTTP, queries ...string) { for _, query := range queries { out := subscribe(client, logger, query) @@ -59,7 +84,6 @@ func (p Proxy) EventListener(host string) { select { case result := <-out: eventChan <- result - case <-client.Quit(): return } @@ -68,12 +92,17 @@ func (p Proxy) EventListener(host string) { } } - // subscribe to events - go subscribeToEvents( + // Subscribe to events for each client + go subscribeToEvents(clients[0], "tm.event = 'NewBlock'", "tm.event = 'Tx' AND message.action='/arkeo.arkeo.MsgOpenContract'", "tm.event = 'Tx' AND message.action='/arkeo.arkeo.MsgCloseContract'", + ) + + go subscribeToEvents(clients[1], "tm.event = 'Tx' AND message.action='/arkeo.arkeo.MsgClaimContractIncome'", + "tm.event = 'Tx' AND message.action='/arkeo.arkeo.MsgBondProvider'", + "tm.event = 'Tx' AND message.action='/arkeo.arkeo.MsgModProvider'", ) dispatchEvents := func(result tmCoreTypes.ResultEvent) { @@ -90,6 +119,12 @@ func (p Proxy) EventListener(host string) { case strings.Contains(result.Query, "MsgClaimContractIncome"): p.handleContractSettlementEvent(result) + case strings.Contains(result.Query, "MsgModProvider"): + p.handleModProviderEvent(result) + + case strings.Contains(result.Query, "MsgBondProvider"): + p.handleBondProviderEvent(result) + default: logger.Error("Unknown Event Type", "Query", result.Query) } @@ -281,3 +316,92 @@ func parseTypedEvent(result tmCoreTypes.ResultEvent, eventType string) (proto.Me return msg, fmt.Errorf("event %s not found", eventType) } + +func (p Proxy) handleBondProviderEvent(result tmCoreTypes.ResultEvent) { + typedEvent, err := parseTypedEvent(result, "arkeo.arkeo.EventBondProvider") + if err != nil { + p.logger.Error("failed to parse typed event", "error", err) + return + } + + evt, ok := typedEvent.(*types.EventBondProvider) + if !ok { + p.logger.Error(fmt.Sprintf("failed to cast %T to EventOpenContract", typedEvent)) + return + } + + service := common.Service(common.ServiceLookup[evt.Service]) + if !p.isMyPubKey(evt.Provider) { + return + } + providerConfig, err := p.ProviderConfigStore.Get(evt.Provider, service.String()) + if err != nil { + p.logger.Info("failed to get provider config, initializing new config", "error", err) + providerConfig = ProviderConfiguration{ + PubKey: evt.Provider, + Service: service, + Bond: evt.BondAbs, + BondRelative: evt.BondRel, + MetadataUri: "", + MetadataNonce: 0, + Status: types.ProviderStatus(0), + MinContractDuration: 0, + MaxContractDuration: 0, + SubscriptionRate: cosmos.Coins{}, + PayAsYouGoRate: cosmos.Coins{}, + SettlementDuration: 0, + } + } + + providerConfig.Bond = evt.BondAbs + providerConfig.BondRelative = evt.BondRel + err = p.ProviderConfigStore.Set(providerConfig) + if err != nil { + p.logger.Error("failed to update provider configuration", "error", err) + return + } + p.logger.Info("Provider configuration updated on bond provider event", "pubkey", evt.Provider.String(), "service", service.String()) +} +func (p Proxy) handleModProviderEvent(result tmCoreTypes.ResultEvent) { + typedEvent, err := parseTypedEvent(result, "arkeo.arkeo.EventModProvider") + if err != nil { + p.logger.Error("failed to parse typed event", "error", err) + return + } + + evt, ok := typedEvent.(*types.EventModProvider) + if !ok { + p.logger.Error(fmt.Sprintf("failed to cast %T to EventOpenContract", typedEvent)) + return + } + + service := common.Service(common.ServiceLookup[evt.Service]) + + if !p.isMyPubKey(evt.Provider) { + return + } + + providerConfig, err := p.ProviderConfigStore.Get(evt.Provider, service.String()) + if err != nil { + p.logger.Error(fmt.Sprintf("failed to get provider %s", err)) + return + } + + providerConfig.Bond = evt.Bond + providerConfig.Service = service + providerConfig.MetadataUri = evt.MetadataUri + providerConfig.MetadataNonce = evt.MetadataNonce + providerConfig.Status = evt.Status + providerConfig.MinContractDuration = evt.MinContractDuration + providerConfig.MaxContractDuration = evt.MaxContractDuration + providerConfig.SubscriptionRate = evt.SubscriptionRate + providerConfig.PayAsYouGoRate = evt.PayAsYouGoRate + providerConfig.SettlementDuration = evt.SettlementDuration + + err = p.ProviderConfigStore.Set(providerConfig) + if err != nil { + p.logger.Error("failed to update provider configuration", "error", err) + return + } + p.logger.Info("Provider configuration updated on mod provider event", "pubkey", evt.Provider.String(), "service", service.String()) +} diff --git a/sentinel/event_stream_test.go b/sentinel/event_stream_test.go index ed753862..58bec924 100644 --- a/sentinel/event_stream_test.go +++ b/sentinel/event_stream_test.go @@ -36,7 +36,8 @@ func newTestConfig() conf.Configuration { func TestHandleOpenContractEvent(t *testing.T) { testConfig := newTestConfig() - proxy := NewProxy(testConfig) + proxy, err := NewProxy(testConfig) + require.NoError(t, err) inputContract := types.Contract{ Provider: testConfig.ProviderPubKey, Service: common.BTCService, @@ -108,7 +109,8 @@ func TestHandleOpenContractEvent(t *testing.T) { func TestHandleCloseContractEvent(t *testing.T) { testConfig := newTestConfig() - proxy := NewProxy(testConfig) + proxy, err := NewProxy(testConfig) + require.NoError(t, err) inputContract := types.Contract{ Provider: testConfig.ProviderPubKey, Service: common.BTCService, @@ -158,7 +160,8 @@ func TestHandleCloseContractEvent(t *testing.T) { func TestHandleHandleContractSettlementEvent(t *testing.T) { testConfig := newTestConfig() - proxy := NewProxy(testConfig) + proxy, err := NewProxy(testConfig) + require.NoError(t, err) inputContract := types.Contract{ Provider: testConfig.ProviderPubKey, Service: common.BTCService, diff --git a/sentinel/provider_store.go b/sentinel/provider_store.go new file mode 100644 index 00000000..012e655e --- /dev/null +++ b/sentinel/provider_store.go @@ -0,0 +1,92 @@ +package sentinel + +import ( + "encoding/json" + "fmt" + + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/syndtr/goleveldb/leveldb" + "github.com/syndtr/goleveldb/leveldb/storage" + + "github.com/arkeonetwork/arkeo/common" + "github.com/arkeonetwork/arkeo/common/cosmos" + "github.com/arkeonetwork/arkeo/x/arkeo/types" +) + +type ProviderConfigurationStore struct { + logger zerolog.Logger + db *leveldb.DB +} + +func NewProviderConfigurationStore(levelDbFolder string) (*ProviderConfigurationStore, error) { + var db *leveldb.DB + var err error + if len(levelDbFolder) == 0 { + log.Warn().Msg("level db folder is empty, create in memory storage") + // no directory given, use in memory store + storage := storage.NewMemStorage() + db, err = leveldb.Open(storage, nil) + if err != nil { + return nil, fmt.Errorf("fail to in memory open level db: %w", err) + } + } else { + db, err = leveldb.OpenFile(levelDbFolder, nil) + if err != nil { + return nil, fmt.Errorf("fail to open level db %s: %w", levelDbFolder, err) + } + } + + return &ProviderConfigurationStore{ + logger: log.With().Str("module", "provider-config-store").Logger(), + db: db, + }, nil +} + +type ProviderConfiguration struct { + PubKey common.PubKey `json:"pubkey,omitempty"` + Service common.Service `json:"service,omitempty"` + Bond cosmos.Int `json:"bond,omitempty"` + BondRelative cosmos.Int `json:"bond_relative,omitempty"` + MetadataUri string `json:"metadata_uri,omitempty"` + MetadataNonce uint64 `json:"metadata_nonce,omitempty"` + Status types.ProviderStatus `json:"status,omitempty"` + MinContractDuration int64 `json:"min_contract_duration,omitempty"` + MaxContractDuration int64 `json:"max_contract_duration,omitempty"` + SubscriptionRate cosmos.Coins `json:"subscription_rate"` + PayAsYouGoRate cosmos.Coins `json:"pay_as_you_go_rate"` + SettlementDuration int64 `json:"settlement_duration,omitempty"` +} + +// GetProviderModOrBondConfig retrieves a ProviderConfiguration by its PubKey +func (ps *ProviderConfigurationStore) Get(pubKey common.PubKey, service string) (ProviderConfiguration, error) { + data, err := ps.db.Get([]byte(pubKey.String()+service), nil) + if err != nil { + return ProviderConfiguration{}, err + } + + var config ProviderConfiguration + err = json.Unmarshal(data, &config) + if err != nil { + return ProviderConfiguration{}, err + } + return config, nil +} + +// SetProviderModOrBondConfig saves or updates a ProviderConfiguration in the database +func (ps *ProviderConfigurationStore) Set(config ProviderConfiguration) error { + data, err := json.Marshal(config) + if err != nil { + return err + } + + err = ps.db.Put([]byte(config.PubKey.String()+config.Service.String()), data, nil) + if err != nil { + return err + } + return nil +} + +func (p *ProviderConfigurationStore) Remove(pubKey common.PubKey, service string) error { + return p.db.Delete([]byte(pubKey.String()+service), nil) +} diff --git a/sentinel/provider_store_test.go b/sentinel/provider_store_test.go new file mode 100644 index 00000000..c1220870 --- /dev/null +++ b/sentinel/provider_store_test.go @@ -0,0 +1,96 @@ +package sentinel + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/arkeonetwork/arkeo/common" + "github.com/arkeonetwork/arkeo/common/cosmos" + "github.com/arkeonetwork/arkeo/x/arkeo/types" +) + +func TestNewProviderConfigurationStore_InMemory(t *testing.T) { + store, err := NewProviderConfigurationStore("") + require.NoError(t, err, "expected no error when creating in-memory store") + require.NotNil(t, store, "expected store to be created") + + err = store.db.Close() + require.NoError(t, err, "expected no error when closing in-memory store") +} + +func TestNewProviderConfigurationStore_FileBased(t *testing.T) { + tempDir := t.TempDir() + store, err := NewProviderConfigurationStore(tempDir) + require.NoError(t, err, "expected no error when creating file-based store") + require.NotNil(t, store, "expected store to be created") + + err = store.db.Close() + require.NoError(t, err, "expected no error when closing file-based store") +} + +func TestProviderConfigurationStore_SetAndGet(t *testing.T) { + store, err := NewProviderConfigurationStore("") + require.NoError(t, err, "expected no error when creating in-memory store") + + pubKey, _ := common.NewPubKey("tarkeopub1addwnpepqfzke9590mrh4m430zapyl3eh0na4ffzrssz89d4qq89ffuy4xn2yqgcm5v") + service := common.Service(common.ServiceLookup["mock"]) + config := ProviderConfiguration{ + PubKey: pubKey, + Service: service, + Bond: cosmos.NewInt(1000), + BondRelative: cosmos.NewInt(100), + MetadataUri: "http://test-metadata.com", + MetadataNonce: 1, + Status: types.ProviderStatus_ONLINE, + MinContractDuration: 10, + MaxContractDuration: 100, + SubscriptionRate: cosmos.Coins{cosmos.NewInt64Coin("arkeo", 50)}, + PayAsYouGoRate: cosmos.Coins{cosmos.NewInt64Coin("arkeo", 5)}, + SettlementDuration: 200, + } + + err = store.Set(config) + require.NoError(t, err, "expected no error when setting provider config") + + retrievedConfig, err := store.Get(pubKey, service.String()) + require.NoError(t, err, "expected no error when getting provider config") + require.Equal(t, config, retrievedConfig, "expected the stored and retrieved configs to be the same") + + err = store.db.Close() + require.NoError(t, err, "expected no error when closing in-memory store") +} + +func TestProviderConfigurationStore_Remove(t *testing.T) { + store, err := NewProviderConfigurationStore("") + require.NoError(t, err, "expected no error when creating in-memory store") + + pubKey, _ := common.NewPubKey("tarkeopub1addwnpepqfzke9590mrh4m430zapyl3eh0na4ffzrssz89d4qq89ffuy4xn2yqgcm5v") + service := common.Service(common.ServiceLookup["mock"]) + config := ProviderConfiguration{ + PubKey: pubKey, + Service: service, + Bond: cosmos.NewInt(1000), + BondRelative: cosmos.NewInt(100), + MetadataUri: "http://test-metadata.com", + MetadataNonce: 1, + Status: types.ProviderStatus_ONLINE, + MinContractDuration: 10, + MaxContractDuration: 100, + SubscriptionRate: cosmos.Coins{cosmos.NewInt64Coin("arkeo", 50)}, + PayAsYouGoRate: cosmos.Coins{cosmos.NewInt64Coin("arkeo", 5)}, + SettlementDuration: 200, + } + + err = store.Set(config) + require.NoError(t, err, "expected no error when setting provider config") + + err = store.Remove(pubKey, service.String()) + require.NoError(t, err, "expected no error when removing provider config") + + _, err = store.Get(pubKey, service.String()) + require.Error(t, err, "expected error when getting non-existent provider config") + + err = store.db.Close() + require.NoError(t, err, "expected no error when closing in-memory store") +} diff --git a/sentinel/routes.go b/sentinel/routes.go index 232aad7e..b4d276b3 100644 --- a/sentinel/routes.go +++ b/sentinel/routes.go @@ -6,4 +6,5 @@ const ( RoutesClaim = "/claim/{id}" RoutesOpenClaims = "/open-claims" RouteManage = "/manage/contract/{id}" + RouteProviderData = "/provider/{service}" ) diff --git a/sentinel/sentinel.go b/sentinel/sentinel.go index 8fd32aa3..f24824e8 100644 --- a/sentinel/sentinel.go +++ b/sentinel/sentinel.go @@ -31,19 +31,28 @@ type Proxy struct { MemStore *MemStore ClaimStore *ClaimStore ContractConfigStore *ContractConfigurationStore + ProviderConfigStore *ProviderConfigurationStore logger log.Logger proxies map[string]*url.URL } -func NewProxy(config conf.Configuration) Proxy { +func NewProxy(config conf.Configuration) (Proxy, error) { logger := log.NewTMLogger(log.NewSyncWriter(os.Stdout)) claimStore, err := NewClaimStore(config.ClaimStoreLocation) if err != nil { - panic(err) + logger.Error(fmt.Sprintf("failed to create claim store with error: %s", err)) + return Proxy{}, fmt.Errorf("failed to create claim store with error: %s", err) } contractConfigStore, err := NewContractConfigurationStore(config.ContractConfigStoreLocation) if err != nil { - panic(err) + logger.Error(fmt.Sprintf("failed to create contract config store with error: %s", err)) + return Proxy{}, fmt.Errorf("failed to create contract config store with error: %s", err) + } + + providerConfigStore, err := NewProviderConfigurationStore(config.ProviderConfigStoreLocation) + if err != nil { + logger.Error(fmt.Sprintf("failed to create provider config store with error: %s", err)) + return Proxy{}, fmt.Errorf("failed to create provider config store with error: %s", err) } return Proxy{ @@ -54,7 +63,8 @@ func NewProxy(config conf.Configuration) Proxy { ContractConfigStore: contractConfigStore, proxies: loadProxies(), logger: logger, - } + ProviderConfigStore: providerConfigStore, + }, nil } func loadProxies() map[string]*url.URL { @@ -324,6 +334,7 @@ func (p Proxy) handleClaim(w http.ResponseWriter, r *http.Request) { claim := NewClaim(contractId, nil, 0, "") claim, err = p.ClaimStore.Get(claim.Key()) + p.logger.Info(fmt.Sprintf("claim data %v", claim)) if err != nil { p.logger.Error("fail to get claim from memstore", "error", err, "key", claim.Key()) respondWithError(w, fmt.Sprintf("fetch contract error: %s", err), http.StatusBadRequest) @@ -415,6 +426,7 @@ func (p *Proxy) getRouter() *mux.Router { router.HandleFunc(RoutesClaim, http.HandlerFunc(p.handleClaim)).Methods(http.MethodGet) router.HandleFunc(RoutesOpenClaims, http.HandlerFunc(p.handleOpenClaims)).Methods(http.MethodGet) router.HandleFunc(RouteManage, http.HandlerFunc(p.handleContract)).Methods(http.MethodGet, http.MethodPost) + router.HandleFunc(RouteProviderData, http.HandlerFunc(p.handleProviderData)).Methods(http.MethodGet) router.PathPrefix("/").Handler( p.auth( handlers.ProxyHeaders( @@ -452,3 +464,24 @@ func respondWithJSON(w http.ResponseWriter, code int, payload interface{}) { w.WriteHeader(code) _, _ = w.Write(response) } + +func (p Proxy) handleProviderData(w http.ResponseWriter, r *http.Request) { + r.Header.Set("Content-Type", "application/json") + vars := mux.Vars(r) + serviceString, ok := vars["service"] + if !ok { + respondWithError(w, "missing service in uri", http.StatusBadRequest) + return + } + service := common.Service(common.ServiceLookup[serviceString]) + + providerConfigData, err := p.ProviderConfigStore.Get(p.Config.ProviderPubKey, service.String()) + if err != nil { + p.logger.Error("failed to get provider details", "error", err, "provider", p.Config.ProviderPubKey) + respondWithError(w, fmt.Sprintf("Invalid Provider: %s", err), http.StatusBadRequest) + return + } + + d, _ := json.Marshal(providerConfigData) + _, _ = w.Write(d) +} diff --git a/sentinel/sentinel_test.go b/sentinel/sentinel_test.go index e67d13da..b308619f 100644 --- a/sentinel/sentinel_test.go +++ b/sentinel/sentinel_test.go @@ -29,7 +29,8 @@ func setUpTest(t *testing.T, pk1, pk2 common.PubKey) *httptest.Server { func TestHandleActiveContract(t *testing.T) { testConfig := newTestConfig() - proxy := NewProxy(testConfig) + proxy, err := NewProxy(testConfig) + require.NoError(t, err) inputContract := types.Contract{ Provider: testConfig.ProviderPubKey, Service: common.BTCService, @@ -84,7 +85,8 @@ func TestHandleActiveContract(t *testing.T) { func TestHandleClaim(t *testing.T) { testConfig := newTestConfig() - proxy := NewProxy(testConfig) + proxy, err := NewProxy(testConfig) + require.NoError(t, err) inputContract := types.Contract{ Provider: testConfig.ProviderPubKey, Service: common.BTCService, @@ -152,7 +154,8 @@ func TestHandleClaim(t *testing.T) { func TestHandleOpenClaims(t *testing.T) { testConfig := newTestConfig() - proxy := NewProxy(testConfig) + proxy, err := NewProxy(testConfig) + require.NoError(t, err) inputContract := types.Contract{ Provider: testConfig.ProviderPubKey, Service: common.BTCService, diff --git a/test/regression/cmd/run.go b/test/regression/cmd/run.go index 8142a096..88ad62c9 100644 --- a/test/regression/cmd/run.go +++ b/test/regression/cmd/run.go @@ -306,6 +306,7 @@ func run(path string) error { "FREE_RATE_LIMIT=10", "CLAIM_STORE_LOCATION=/regtest/.arkeo/claims", "CONTRACT_CONFIG_STORE_LOCATION=/regtest/.arkeo/contract_configs", + "PROVIDER_CONFIG_STORE_LOCATION=/regtest/.arkeo/provider_configs", }, sigkill: syscall.SIGKILL, },