Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions cmd/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,7 @@ func (di *Dependencies) bootstrapTequilapi(nodeOptions node.Options, listener ne
tequilapi_endpoints.AddRoutesForCurrencyExchange(di.PilvytisAPI),
tequilapi_endpoints.AddRoutesForPilvytis(di.PilvytisAPI, di.PilvytisOrderIssuer, di.LocationResolver),
tequilapi_endpoints.AddRoutesForTerms,
tequilapi_endpoints.AddEntertainmentRoutes(entertainment.NewEstimator(
config.FlagPaymentPriceGiB.Value,
config.FlagPaymentPriceHour.Value,
)),
tequilapi_endpoints.AddEntertainmentRoutes(entertainment.NewMarketEstimator(di.PricingHelper)),
tequilapi_endpoints.AddRoutesForValidator,
},
)
Expand Down
5 changes: 1 addition & 4 deletions cmd/bootstrap_mobile_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,10 +92,7 @@ func (di *Dependencies) bootstrapTequilapi(nodeOptions node.Options, listener ne
tequilapi_endpoints.AddRoutesForCurrencyExchange(di.PilvytisAPI),
tequilapi_endpoints.AddRoutesForPilvytis(di.PilvytisAPI, di.PilvytisOrderIssuer, di.LocationResolver),
tequilapi_endpoints.AddRoutesForTerms,
tequilapi_endpoints.AddEntertainmentRoutes(entertainment.NewEstimator(
config.FlagPaymentPriceGiB.Value,
config.FlagPaymentPriceHour.Value,
)),
tequilapi_endpoints.AddEntertainmentRoutes(entertainment.NewMarketEstimator(di.PricingHelper)),
tequilapi_endpoints.AddRoutesForValidator,
},
)
Expand Down
40 changes: 26 additions & 14 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import (
"github.com/BurntSushi/toml"
"github.com/mysteriumnetwork/node/eventbus"
"github.com/mysteriumnetwork/node/metadata"
"github.com/mysteriumnetwork/node/utils/jsonutil"
"github.com/pkg/errors"
"github.com/rs/zerolog/log"
"github.com/spf13/cast"
Expand Down Expand Up @@ -86,11 +85,9 @@ func (cfg *Config) LoadUserConfig(location string) error {
if err != nil {
return errors.Wrap(err, "failed to decode configuration file")
}
cfgJson, err := jsonutil.ToJson(cfg.user)
if err != nil {
return err
}
log.Info().Msg("User configuration loaded: \n" + cfgJson)
// Do not log the configuration contents. User configuration contains
// credentials (for example mmn.api-key) which must never reach logs.
log.Info().Msg("User configuration loaded")
return nil
}

Expand All @@ -111,11 +108,8 @@ func (cfg *Config) SaveUserConfig() error {
if err != nil {
return errors.Wrap(err, "failed to write configuration to file")
}
cfgJson, err := jsonutil.ToJson(cfg.user)
if err != nil {
return err
}
log.Info().Msg("User configuration written: \n" + cfgJson)
// Do not log the configuration contents. See LoadUserConfig.
log.Info().Msg("User configuration written")
return nil
}

Expand Down Expand Up @@ -213,19 +207,37 @@ func (cfg *Config) Get(key string) interface{} {
defer cfg.mu.RUnlock()
cliValue := SearchMap(cfg.cli, segments)
if cliValue != nil {
log.Debug().Msgf("Returning CLI value %v:%v", key, cliValue)
logConfigValue("CLI", key, cliValue)
return copyValue(cliValue)
}
userValue := SearchMap(cfg.user, segments)
if userValue != nil {
log.Debug().Msgf("Returning user config value %v:%v", key, userValue)
logConfigValue("user config", key, userValue)
return copyValue(userValue)
}
defaultValue := SearchMap(cfg.defaults, segments)
log.Trace().Msgf("Returning default value %v:%v", key, defaultValue)
if isSensitiveConfigKey(key) {
log.Trace().Msgf("Returning default value %v:[REDACTED]", key)
} else {
log.Trace().Msgf("Returning default value %v:%v", key, defaultValue)
}
return copyValue(defaultValue)
}

func logConfigValue(source, key string, value interface{}) {
if isSensitiveConfigKey(key) {
log.Debug().Msgf("Returning %s value %v:[REDACTED]", source, key)
return
}
log.Debug().Msgf("Returning %s value %v:%v", source, key, value)
}

func isSensitiveConfigKey(key string) bool {
normalized := strings.ToLower(key)
normalized = strings.NewReplacer("-", "", "_", "", ".", "").Replace(normalized)
return strings.HasSuffix(normalized, "apikey")
}

// returns scalar values as is. deep-copies maps.
func copyValue(value interface{}) interface{} {
switch v := value.(type) {
Expand Down
7 changes: 7 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ func TestUserConfig_Get(t *testing.T) {
assert.Equal(t, 1003, cfg.Get("openvpn.port"))
}

func TestSensitiveConfigKey(t *testing.T) {
assert.True(t, isSensitiveConfigKey("mmn.api-key"))
assert.True(t, isSensitiveConfigKey("service.api_key"))
assert.True(t, isSensitiveConfigKey("service.apikey"))
assert.False(t, isSensitiveConfigKey("mmn.api-address"))
}

func TestUserConfig_GetConfig(t *testing.T) {
cfg := NewConfig()

Expand Down
5 changes: 4 additions & 1 deletion config/remote/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,10 @@ func (rc *Config) RefreshRemoteConfig() error {
func (rc *Config) Get(key string) interface{} {
segments := strings.Split(strings.ToLower(key), ".")
value := config.SearchMap(rc.config, segments)
log.Debug().Msgf("Returning remote config value %v:%v", key, value)
// Remote configuration may contain credentials. Unlike the local Config,
// this package does not own a complete list of sensitive keys, so avoid
// logging values altogether.
log.Debug().Msgf("Returning remote config value for key %v", key)
return value
}

Expand Down
50 changes: 44 additions & 6 deletions consumer/entertainment/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,13 @@

package entertainment

import "math"
import (
"math"

"github.com/mysteriumnetwork/node/market"
"github.com/mysteriumnetwork/payments/crypto"
"github.com/rs/zerolog/log"
)

const (
video720pMBPerMin = 15
Expand All @@ -30,18 +36,32 @@ type Estimates struct {
VideoMinutes uint64
MusicMinutes uint64
BrowsingMinutes uint64
TrafficMB uint64
PricePerGiB float64
PricePerMin float64
// TrafficMB is decimal megabytes (1 MB = 1,000,000 bytes), not MiB.
TrafficMB uint64
PricePerGiB float64
PricePerMin float64
}

// PriceProvider supplies current consumer prices from the node's shared pricing cache.
type PriceProvider interface {
GetCurrentPrice(nodeType, country, serviceType string) (market.Price, error)
}

// Estimator stores average provider prices to estimate entertainment estimates
// Estimator estimates usage from consumer prices.
type Estimator struct {
prices PriceProvider
pricePerGiB float64
pricePerMin float64
}

// NewEstimator constructor
// NewMarketEstimator uses current residential WireGuard pricing on each estimate.
// No destination is selected by these APIs, so use the market's global default
// rather than a country-specific rate or the consumer's own location.
func NewMarketEstimator(prices PriceProvider) *Estimator {
return &Estimator{prices: prices}
}

// NewEstimator constructs an estimator with explicit MYST/GiB and MYST/minute rates.
func NewEstimator(pricePerGiB, pricePerMin float64) *Estimator {
return &Estimator{
pricePerGiB: pricePerGiB,
Expand All @@ -51,6 +71,24 @@ func NewEstimator(pricePerGiB, pricePerMin float64) *Estimator {

// EstimatedEntertainment calculates average service times
func (e *Estimator) EstimatedEntertainment(myst float64) Estimates {
if e.prices != nil {
price, err := e.prices.GetCurrentPrice("residential", "", "wireguard")
if err != nil {
log.Warn().Err(err).Msg("could not obtain entertainment pricing")
return Estimates{}
}
if price.PricePerGiB == nil || price.PricePerHour == nil {
return Estimates{}
}
// Keep rates local: estimates may run concurrently with pricing updates.
return NewEstimator(crypto.BigMystToFloat(price.PricePerGiB),
crypto.BigMystToFloat(price.PricePerHour)/60).EstimatedEntertainment(myst)
}
if myst < 0 || math.IsNaN(myst) || math.IsInf(myst, 0) ||
e.pricePerGiB <= 0 || math.IsNaN(e.pricePerGiB) || math.IsInf(e.pricePerGiB, 0) ||
e.pricePerMin < 0 || math.IsNaN(e.pricePerMin) || math.IsInf(e.pricePerMin, 0) {
return Estimates{}
}
return Estimates{
VideoMinutes: e.minutes(myst, video720pMBPerMin),
MusicMinutes: e.minutes(myst, audioNormalMBPerMin),
Expand Down
60 changes: 60 additions & 0 deletions consumer/entertainment/estimator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@
package entertainment

import (
"errors"
"math"
"testing"

"github.com/mysteriumnetwork/node/market"
"github.com/mysteriumnetwork/payments/crypto"

"github.com/stretchr/testify/assert"
)

Expand All @@ -46,3 +51,58 @@ func TestEstimator(t *testing.T) {
assert.Less(t, uint64(0), e.MusicMinutes)
assert.Less(t, uint64(0), e.BrowsingMinutes)
}

// The provider is queried for every estimate, so a market refresh is reflected
// without restarting either the mobile node or TequilAPI.
type testPriceProvider struct {
t *testing.T
price market.Price
err error
}

func (p *testPriceProvider) GetCurrentPrice(nodeType, country, serviceType string) (market.Price, error) {
assert.Equal(p.t, "residential", nodeType)
assert.Empty(p.t, country)
assert.Equal(p.t, "wireguard", serviceType)
return p.price, p.err
}

func TestMarketEstimator(t *testing.T) {
provider := &testPriceProvider{t: t, price: market.Price{
PricePerGiB: crypto.FloatToBigMyst(3.73),
PricePerHour: crypto.FloatToBigMyst(0.06),
}}
estimator := NewMarketEstimator(provider)
estimate := estimator.EstimatedEntertainment(29.444)
assert.InDelta(t, 3.73, estimate.PricePerGiB, 1e-12)
assert.InDelta(t, 0.001, estimate.PricePerMin, 1e-12)
// Convert decimal MB back to GiB, allowing truncation of less than 1 MB.
assert.InDelta(t, 29.444/3.73, float64(estimate.TrafficMB)*1e6/(1<<30), 0.001)
assert.Equal(t, uint64(math.Floor(29.444/(15e6/(1<<30)*3.73+0.001))), estimate.VideoMinutes)

provider.price.PricePerGiB = crypto.FloatToBigMyst(7.46)
refreshed := estimator.EstimatedEntertainment(29.444)
assert.InDelta(t, float64(estimate.TrafficMB)/2, float64(refreshed.TrafficMB), 1)
assert.InDelta(t, 7.46, refreshed.PricePerGiB, 1e-12)
}

func TestEstimatorUnits(t *testing.T) {
assert.Equal(t, 1048.576, mib2MB(1000))
assert.Equal(t, 1000.0, mb2MiB(1048.576))
estimate := NewEstimator(1, 0).EstimatedEntertainment(1)
assert.Equal(t, uint64(1073), estimate.TrafficMB) // 1 GiB = 1073.741824 MB
assert.Equal(t, uint64(71), estimate.VideoMinutes) // 15 decimal MB/minute
}

func TestMarketEstimatorUnavailablePrice(t *testing.T) {
provider := &testPriceProvider{t: t, err: errors.New("unavailable")}
assert.Equal(t, Estimates{}, NewMarketEstimator(provider).EstimatedEntertainment(29.444))
provider.err = nil
assert.Equal(t, Estimates{}, NewMarketEstimator(provider).EstimatedEntertainment(29.444))
}

func TestEstimatorInvalidAmount(t *testing.T) {
for _, amount := range []float64{-1, math.NaN(), math.Inf(1)} {
assert.Equal(t, Estimates{}, NewEstimator(3.73, 0).EstimatedEntertainment(amount))
}
}
3 changes: 2 additions & 1 deletion mmn/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ type client struct {

// ClaimNode does an HTTP call to MMN and registers node
func (m *client) ClaimNode(info NodeClaimRequest) error {
log.Debug().Msgf("Registering node to MMN: %+v", info)
// APIKey is intentionally omitted: this request contains an MMN credential.
log.Debug().Msgf("Registering node to MMN: identity=%s vendor_id=%s os=%s arch=%s node_version=%s", info.Identity, info.VendorID, info.OS, info.Arch, info.NodeVersion)

id := identity.FromAddress(info.Identity)
req, err := requests.NewSignedPostRequest(m.mmnAddress, "node", info, m.signer(id))
Expand Down
25 changes: 11 additions & 14 deletions mobile/mysterium/entrypoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -420,20 +420,17 @@ func NewNode(appPath string, options *MobileNodeOptions) (*MobileNode, error) {
di.NATProber,
time.Duration(options.CacheTTLSeconds)*time.Second,
),
pilvytis: di.PilvytisAPI,
pilvytisOrderIssuer: di.PilvytisOrderIssuer,
startTime: time.Now(),
chainID: nodeOptions.OptionsNetwork.ChainID,
sessionStorage: di.SessionStorage,
identityMover: di.IdentityMover,
entertainmentEstimator: entertainment.NewEstimator(
config.FlagPaymentPriceGiB.Value,
config.FlagPaymentPriceHour.Value,
),
residentCountry: di.ResidentCountry,
filterPresetStorage: di.FilterPresetStorage,
hermesMigrator: di.HermesMigrator,
earningsProvider: di.HermesChannelRepository,
pilvytis: di.PilvytisAPI,
pilvytisOrderIssuer: di.PilvytisOrderIssuer,
startTime: time.Now(),
chainID: nodeOptions.OptionsNetwork.ChainID,
sessionStorage: di.SessionStorage,
identityMover: di.IdentityMover,
entertainmentEstimator: entertainment.NewMarketEstimator(di.PricingHelper),
residentCountry: di.ResidentCountry,
filterPresetStorage: di.FilterPresetStorage,
hermesMigrator: di.HermesMigrator,
earningsProvider: di.HermesChannelRepository,
}

if options.IsProvider {
Expand Down
8 changes: 5 additions & 3 deletions mobile/mysterium/estimator.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,11 @@ type Estimates struct {
VideoMinutes int64
MusicMinutes int64
BrowsingMinutes int64
TrafficMB int64
PricePerGB float64
PricePerMin float64
// TrafficMB is decimal MB; convert to GiB with TrafficMB * 1e6 / (1 << 30).
TrafficMB int64
// PricePerGB is MYST per GiB; the legacy field name is kept for mobile compatibility.
PricePerGB float64
PricePerMin float64
}

func newEstimates(e entertainment.Estimates) *Estimates {
Expand Down
13 changes: 7 additions & 6 deletions tequilapi/contract/entertainment.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@ func (req *EntertainmentEstimateRequest) Bind(request *http.Request) *apierror.A
// EntertainmentEstimateResponse represents estimated entertainment.
// swagger:model EntertainmentEstimateResponse
type EntertainmentEstimateResponse struct {
VideoMinutes uint64 `json:"video_minutes"`
MusicMinutes uint64 `json:"music_minutes"`
BrowsingMinutes uint64 `json:"browsing_minutes"`
TrafficMB uint64 `json:"traffic_mb"`
PriceGiB float64 `json:"price_gib"`
PriceMin float64 `json:"price_min"`
VideoMinutes uint64 `json:"video_minutes"`
MusicMinutes uint64 `json:"music_minutes"`
BrowsingMinutes uint64 `json:"browsing_minutes"`
// TrafficMB is decimal MB; convert to GiB with TrafficMB * 1e6 / (1 << 30).
TrafficMB uint64 `json:"traffic_mb"`
PriceGiB float64 `json:"price_gib"`
PriceMin float64 `json:"price_min"`
}
Loading