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 }