code.vegaprotocol.io/vega@v0.79.0/core/datasource/spec/engine.go (about)

     1  // Copyright (C) 2023 Gobalsky Labs Limited
     2  //
     3  // This program is free software: you can redistribute it and/or modify
     4  // it under the terms of the GNU Affero General Public License as
     5  // published by the Free Software Foundation, either version 3 of the
     6  // License, or (at your option) any later version.
     7  //
     8  // This program is distributed in the hope that it will be useful,
     9  // but WITHOUT ANY WARRANTY; without even the implied warranty of
    10  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    11  // GNU Affero General Public License for more details.
    12  //
    13  // You should have received a copy of the GNU Affero General Public License
    14  // along with this program.  If not, see <http://www.gnu.org/licenses/>.
    15  
    16  package spec
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"sort"
    22  	"strconv"
    23  	"strings"
    24  	"time"
    25  
    26  	"code.vegaprotocol.io/vega/core/datasource"
    27  	"code.vegaprotocol.io/vega/core/datasource/common"
    28  	"code.vegaprotocol.io/vega/core/events"
    29  	"code.vegaprotocol.io/vega/logging"
    30  	vegapb "code.vegaprotocol.io/vega/protos/vega"
    31  	datapb "code.vegaprotocol.io/vega/protos/vega/data/v1"
    32  )
    33  
    34  // Broker interface. Do not need to mock (use package broker/mock).
    35  type Broker interface {
    36  	Stage(event events.Event)
    37  	Send(event events.Event)
    38  	SendBatch(events []events.Event)
    39  }
    40  
    41  // TimeService interface.
    42  //
    43  //go:generate go run github.com/golang/mock/mockgen -destination mocks/time_service_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/spec TimeService
    44  type TimeService interface {
    45  	GetTimeNow() time.Time
    46  }
    47  
    48  // The verifier and filterer need to know about new spec immediately, waiting on the event will lead to spec not found issues
    49  //
    50  //go:generate go run github.com/golang/mock/mockgen -destination mocks/spec_activations_listener.go -package mocks code.vegaprotocol.io/vega/core/datasource/spec SpecActivationsListener
    51  type SpecActivationsListener interface {
    52  	OnSpecActivated(context.Context, datasource.Spec) error
    53  	OnSpecDeactivated(context.Context, datasource.Spec)
    54  }
    55  
    56  // Engine is responsible for broadcasting the Data to products and risk
    57  // models interested in it.
    58  type Engine struct {
    59  	log                     *logging.Logger
    60  	timeService             TimeService
    61  	broker                  Broker
    62  	subscriptions           *specSubscriptions
    63  	specActivationListeners []SpecActivationsListener
    64  }
    65  
    66  // NewEngine creates a new Engine.
    67  func NewEngine(
    68  	log *logging.Logger,
    69  	conf Config,
    70  	ts TimeService,
    71  	broker Broker,
    72  ) *Engine {
    73  	log = log.Named(namedLogger)
    74  	log.SetLevel(conf.Level.Get())
    75  
    76  	e := &Engine{
    77  		log:           log,
    78  		timeService:   ts,
    79  		broker:        broker,
    80  		subscriptions: newSpecSubscriptions(),
    81  	}
    82  
    83  	return e
    84  }
    85  
    86  // ListensToSigners checks if the signatures (pubkeys, ETH addresses) from provided sourcing Data are among the keys
    87  // current Specs listen to.
    88  func (e *Engine) ListensToSigners(data common.Data) bool {
    89  	return e.subscriptions.hasAnySubscribers(func(spec Spec) bool {
    90  		return spec.MatchSigners(data)
    91  	})
    92  }
    93  
    94  func (e *Engine) AddSpecActivationListener(listener SpecActivationsListener) {
    95  	e.specActivationListeners = append(e.specActivationListeners, listener)
    96  }
    97  
    98  func (e *Engine) HasMatch(data common.Data) (bool, error) {
    99  	result, err := e.subscriptions.filterSubscribers(func(spec Spec) (bool, error) {
   100  		return spec.MatchData(data)
   101  	})
   102  	if err != nil {
   103  		return false, err
   104  	}
   105  	return result.hasMatched(), nil
   106  }
   107  
   108  // BroadcastData broadcasts data to products and risk models that are interested
   109  // in it. If no one is listening to this Data, it is discarded.
   110  func (e *Engine) BroadcastData(ctx context.Context, data common.Data) error {
   111  	result, err := e.subscriptions.filterSubscribers(func(spec Spec) (bool, error) {
   112  		return spec.MatchData(data)
   113  	})
   114  	if err != nil {
   115  		e.log.Debug("error in filtering subscribers",
   116  			logging.Error(err),
   117  		)
   118  		return err
   119  	}
   120  
   121  	if !result.hasMatched() {
   122  		if e.log.IsDebug() {
   123  			strs := make([]string, 0, len(data.Data))
   124  			for k, v := range data.Data {
   125  				strs = append(strs, fmt.Sprintf("%s:%s", k, v))
   126  			}
   127  			e.log.Debug(
   128  				"no subscriber matches the oracle data",
   129  				logging.Strings("signers", common.SignersToStringList(data.Signers)),
   130  				logging.String("data", strings.Join(strs, ", ")),
   131  			)
   132  		}
   133  		return nil
   134  	}
   135  	// we have a match, ensure vega-time meta is set.
   136  	if _, err := data.GetDataTimestampNano(); err != nil {
   137  		if data.MetaData == nil {
   138  			data.MetaData = map[string]string{}
   139  		}
   140  		data.MetaData["vega-time"] = strconv.FormatInt(e.timeService.GetTimeNow().Unix(), 10)
   141  	}
   142  
   143  	for _, subscriber := range result.subscribers {
   144  		if err := subscriber(ctx, data); err != nil {
   145  			e.log.Debug("broadcasting data to subscriber failed",
   146  				logging.Error(err),
   147  			)
   148  		}
   149  	}
   150  	e.sendMatchedData(ctx, data, result.specIDs)
   151  
   152  	return nil
   153  }
   154  
   155  // Subscribe registers a callback for a given Spec that is called when an
   156  // signedoracle Data matches the spec.
   157  // It returns a SubscriptionID that is used to Unsubscribe.
   158  // If cb is nil, the method panics.
   159  func (e *Engine) Subscribe(ctx context.Context, spec Spec, cb OnMatchedData) (SubscriptionID, Unsubscriber, error) {
   160  	if cb == nil {
   161  		panic(fmt.Sprintf("a callback is required for spec %v", spec))
   162  	}
   163  	updatedSubscription, firstSubscription := e.subscriptions.addSubscriber(spec, cb, e.timeService.GetTimeNow())
   164  	if firstSubscription {
   165  		for _, listener := range e.specActivationListeners {
   166  			err := listener.OnSpecActivated(ctx, *spec.OriginalSpec)
   167  			if err != nil {
   168  				return 0, nil, fmt.Errorf("failed to activate spec: %w", err)
   169  			}
   170  		}
   171  	}
   172  
   173  	e.sendNewSpecSubscription(ctx, updatedSubscription)
   174  	return updatedSubscription.subscriptionID, func(ctx context.Context, id SubscriptionID) {
   175  		e.Unsubscribe(ctx, id)
   176  	}, nil
   177  }
   178  
   179  // Unsubscribe unregisters the callback associated to the SubscriptionID.
   180  // If the id doesn't exist, this method panics.
   181  func (e *Engine) Unsubscribe(ctx context.Context, id SubscriptionID) {
   182  	updatedSubscription, hasNoMoreSubscriber := e.subscriptions.removeSubscriber(id)
   183  	if hasNoMoreSubscriber {
   184  		e.sendSpecDeactivation(ctx, updatedSubscription)
   185  		for _, listener := range e.specActivationListeners {
   186  			listener.OnSpecDeactivated(ctx, updatedSubscription.spec)
   187  		}
   188  	}
   189  }
   190  
   191  // sendNewSpecSubscription send an event to the broker to inform of the
   192  // subscription (and thus activation) to a spec.
   193  // This may be a subscription to a brand-new spec, or an additional one.
   194  func (e *Engine) sendNewSpecSubscription(ctx context.Context, update updatedSubscription) {
   195  	proto := &vegapb.ExternalDataSourceSpec{
   196  		Spec: update.spec.IntoProto(),
   197  	}
   198  	proto.Spec.CreatedAt = update.specActivatedAt.UnixNano()
   199  	proto.Spec.Status = vegapb.DataSourceSpec_STATUS_ACTIVE
   200  
   201  	e.broker.Send(events.NewOracleSpecEvent(ctx, &vegapb.OracleSpec{ExternalDataSourceSpec: proto}))
   202  }
   203  
   204  // sendSpecDeactivation send an event to the broker to inform of
   205  // the deactivation (and thus activation) to a spec.
   206  // This may be a subscription to a brand-new spec, or an additional one.
   207  func (e *Engine) sendSpecDeactivation(ctx context.Context, update updatedSubscription) {
   208  	proto := &vegapb.ExternalDataSourceSpec{
   209  		Spec: update.spec.IntoProto(),
   210  	}
   211  
   212  	proto.Spec.CreatedAt = update.specActivatedAt.UnixNano()
   213  	proto.Spec.Status = vegapb.DataSourceSpec_STATUS_DEACTIVATED
   214  	e.broker.Send(events.NewOracleSpecEvent(ctx, &vegapb.OracleSpec{ExternalDataSourceSpec: proto}))
   215  }
   216  
   217  // sendMatchedData send an event to the broker to inform of
   218  // a match between a specific data source data and one or several specs.
   219  func (e *Engine) sendMatchedData(ctx context.Context, data common.Data, specIDs []SpecID) {
   220  	payload := make([]*datapb.Property, 0, len(data.Data))
   221  	for name, value := range data.Data {
   222  		payload = append(payload, &datapb.Property{
   223  			Name:  name,
   224  			Value: value,
   225  		})
   226  	}
   227  	sort.Slice(payload, func(i, j int) bool {
   228  		return strings.Compare(payload[i].Name, payload[j].Name) < 0
   229  	})
   230  
   231  	metaData := make([]*datapb.Property, 0, len(data.MetaData))
   232  	for name, value := range data.MetaData {
   233  		metaData = append(metaData, &datapb.Property{
   234  			Name:  name,
   235  			Value: value,
   236  		})
   237  	}
   238  	sort.Slice(metaData, func(i, j int) bool {
   239  		return strings.Compare(metaData[i].Name, metaData[j].Name) < 0
   240  	})
   241  
   242  	ids := make([]string, 0, len(specIDs))
   243  	for _, specID := range specIDs {
   244  		ids = append(ids, string(specID))
   245  	}
   246  
   247  	sigs := make([]*datapb.Signer, len(data.Signers))
   248  	for i, s := range data.Signers {
   249  		sigs[i] = s.IntoProto()
   250  	}
   251  
   252  	dataProto := vegapb.OracleData{
   253  		ExternalData: &datapb.ExternalData{
   254  			Data: &datapb.Data{
   255  				Signers:        sigs,
   256  				Data:           payload,
   257  				MatchedSpecIds: ids,
   258  				BroadcastAt:    e.timeService.GetTimeNow().UnixNano(),
   259  				MetaData:       metaData,
   260  			},
   261  		},
   262  	}
   263  	e.broker.Send(events.NewOracleDataEvent(ctx, vegapb.OracleData{ExternalData: dataProto.ExternalData}))
   264  }