code.vegaprotocol.io/vega@v0.79.0/core/datasource/external/ethverifier/verifier.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 ethverifier 17 18 import ( 19 "bytes" 20 "context" 21 "fmt" 22 "strconv" 23 "sync" 24 "time" 25 26 "code.vegaprotocol.io/vega/core/datasource/common" 27 "code.vegaprotocol.io/vega/core/datasource/errors" 28 "code.vegaprotocol.io/vega/core/datasource/external/ethcall" 29 "code.vegaprotocol.io/vega/core/events" 30 "code.vegaprotocol.io/vega/core/metrics" 31 "code.vegaprotocol.io/vega/core/types" 32 "code.vegaprotocol.io/vega/core/validators" 33 "code.vegaprotocol.io/vega/logging" 34 vegapb "code.vegaprotocol.io/vega/protos/vega" 35 datapb "code.vegaprotocol.io/vega/protos/vega/data/v1" 36 37 "github.com/emirpasic/gods/sets/treeset" 38 ) 39 40 const keepHashesDuration = 24 * 2 * time.Hour 41 42 //go:generate go run github.com/golang/mock/mockgen -destination mocks/witness_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethverifier Witness 43 type Witness interface { 44 StartCheckWithDelay(validators.Resource, func(interface{}, bool), time.Time, int64) error 45 RestoreResource(validators.Resource, func(interface{}, bool)) error 46 } 47 48 //go:generate go run github.com/golang/mock/mockgen -destination mocks/oracle_data_broadcaster_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethverifier OracleDataBroadcaster 49 type OracleDataBroadcaster interface { 50 BroadcastData(context.Context, common.Data) error 51 } 52 53 //go:generate go run github.com/golang/mock/mockgen -destination mocks/eth_confirmations_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethverifier EthereumConfirmations 54 type EthereumConfirmations interface { 55 CheckRequiredConfirmations(block uint64, required uint64) error 56 Check(block uint64) error 57 GetConfirmations() uint64 58 } 59 60 //go:generate go run github.com/golang/mock/mockgen -destination mocks/ethcallengine_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethverifier EthCallEngine 61 type EthCallEngine interface { 62 MakeResult(specID string, bytes []byte) (ethcall.Result, error) 63 CallSpec(ctx context.Context, id string, atBlock uint64) (ethcall.Result, error) 64 GetEthTime(ctx context.Context, atBlock uint64) (uint64, error) 65 GetRequiredConfirmations(specId string) (uint64, error) 66 GetInitialTriggerTime(id string) (uint64, error) 67 StartAtHeight(height uint64, timestamp uint64) 68 Start() 69 } 70 71 type CallEngine interface { 72 MakeResult(specID string, bytes []byte) (ethcall.Result, error) 73 CallSpec(ctx context.Context, id string, atBlock uint64) (ethcall.Result, error) 74 } 75 76 //go:generate go run github.com/golang/mock/mockgen -destination mocks/ethcall_result.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethverifier Result 77 type Result interface { 78 Bytes() []byte 79 Values() ([]any, error) 80 Normalised() (map[string]string, error) 81 PassesFilters() (bool, error) 82 HasRequiredConfirmations() bool 83 } 84 85 // Broker interface. Do not need to mock (use package broker/mock). 86 type Broker interface { 87 Send(event events.Event) 88 SendBatch(events []events.Event) 89 } 90 91 // TimeService interface. 92 // 93 //go:generate go run github.com/golang/mock/mockgen -destination mocks/time_service_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethverifier TimeService 94 type TimeService interface { 95 GetTimeNow() time.Time 96 } 97 98 type Verifier struct { 99 log *logging.Logger 100 101 witness Witness 102 timeService TimeService 103 broker Broker 104 oracleEngine OracleDataBroadcaster 105 ethEngine EthCallEngine 106 ethConfirmations EthereumConfirmations 107 isValidator bool 108 109 pendingCallEvents []*pendingCallEvent 110 finalizedCallResults []*ethcall.ContractCallEvent 111 112 // the eth block height of the last seen ethereum TX 113 lastBlock *types.EthBlock 114 // the eth block height when we did the patch upgrade to fix the missing seen map 115 patchBlock *types.EthBlock 116 117 mu sync.Mutex 118 ackedEvts *ackedEvents 119 } 120 121 type pendingCallEvent struct { 122 callEvent ethcall.ContractCallEvent 123 check func(ctx context.Context) error 124 } 125 126 func (p pendingCallEvent) GetID() string { return p.callEvent.Hash() } 127 func (p pendingCallEvent) GetChainID() string { 128 if p.callEvent.SourceChainID == nil { 129 return "" 130 } 131 return strconv.Itoa(int(*p.callEvent.SourceChainID)) 132 } 133 134 func (p pendingCallEvent) GetType() types.NodeVoteType { 135 return types.NodeVoteTypeEthereumContractCallResult 136 } 137 138 func (p *pendingCallEvent) Check(ctx context.Context) error { return p.check(ctx) } 139 140 func New( 141 log *logging.Logger, 142 witness Witness, 143 ts TimeService, 144 broker Broker, 145 oracleBroadcaster OracleDataBroadcaster, 146 ethCallEngine EthCallEngine, 147 ethConfirmations EthereumConfirmations, 148 isValidator bool, 149 ) (sv *Verifier) { 150 log = log.Named("ethereum-oracle-verifier") 151 s := &Verifier{ 152 log: log, 153 witness: witness, 154 timeService: ts, 155 broker: broker, 156 oracleEngine: oracleBroadcaster, 157 ethEngine: ethCallEngine, 158 ethConfirmations: ethConfirmations, 159 isValidator: isValidator, 160 ackedEvts: &ackedEvents{ 161 timeService: ts, 162 events: treeset.NewWith(ackedEvtBucketComparator), 163 }, 164 } 165 return s 166 } 167 168 func (s *Verifier) ensureNotTooOld(callEvent ethcall.ContractCallEvent) bool { 169 s.mu.Lock() 170 defer s.mu.Unlock() 171 172 if s.patchBlock != nil && callEvent.BlockHeight < s.patchBlock.Height { 173 return false 174 } 175 176 tt := time.Unix(int64(callEvent.BlockTime), 0) 177 removeBefore := s.timeService.GetTimeNow().Add(-keepHashesDuration) 178 179 return !tt.Before(removeBefore) 180 } 181 182 func (s *Verifier) ensureNotDuplicate(callEvent ethcall.ContractCallEvent) bool { 183 s.mu.Lock() 184 defer s.mu.Unlock() 185 186 if s.ackedEvts.Contains(callEvent.Hash()) { 187 return false 188 } 189 190 s.ackedEvts.AddAt(int64(callEvent.BlockTime), callEvent.Hash()) 191 return true 192 } 193 194 func (s *Verifier) getConfirmations(callEvent ethcall.ContractCallEvent) (uint64, error) { 195 if !callEvent.Heartbeat { 196 return s.ethEngine.GetRequiredConfirmations(callEvent.SpecId) 197 } 198 199 if !s.isValidator { 200 // non-validator doesn't know, and doesn't need to know 201 return 0, nil 202 } 203 return s.ethConfirmations.GetConfirmations(), nil 204 } 205 206 // TODO: non finalized events could cause a memory leak, this needs to be addressed in a way that will prevent processing 207 // duplicates but not result in a memory leak, agreed to postpone for now (other verifiers have the same issue) 208 209 func (s *Verifier) ProcessEthereumContractCallResult(callEvent ethcall.ContractCallEvent) error { 210 if !s.ensureNotTooOld(callEvent) { 211 s.log.Error("historic ethereum event received", 212 logging.String("event", fmt.Sprintf("%+v", callEvent))) 213 return errors.ErrEthereumCallEventTooOld 214 } 215 216 if ok := s.ensureNotDuplicate(callEvent); !ok { 217 s.log.Error("ethereum call event already exists", 218 logging.String("event", fmt.Sprintf("%+v", callEvent))) 219 return errors.ErrDuplicatedEthereumCallEvent 220 } 221 222 pending := &pendingCallEvent{ 223 callEvent: callEvent, 224 check: func(ctx context.Context) error { return s.checkCallEventResult(ctx, callEvent) }, 225 } 226 227 confirmations, err := s.getConfirmations(callEvent) 228 if err != nil { 229 return err 230 } 231 232 s.pendingCallEvents = append(s.pendingCallEvents, pending) 233 234 s.log.Info("ethereum call event received, starting validation", 235 logging.String("call-event", fmt.Sprintf("%+v", callEvent))) 236 237 // Timeout for the check set to 1 day, to allow for validator outage scenarios 238 err = s.witness.StartCheckWithDelay( 239 pending, s.onCallEventVerified, s.timeService.GetTimeNow().Add(30*time.Minute), int64(confirmations)) 240 if err != nil { 241 s.log.Error("could not start witness routine", logging.String("id", pending.GetID())) 242 s.removePendingCallEvent(pending.GetID()) 243 } 244 245 metrics.DataSourceEthVerifierCallGaugeAdd(1, callEvent.SpecId) 246 247 return err 248 } 249 250 func (s *Verifier) checkCallEventResult(ctx context.Context, callEvent ethcall.ContractCallEvent) error { 251 metrics.DataSourceEthVerifierCallCounterInc(callEvent.SpecId) 252 253 // Ensure that the ethtime on the call event matches the block number on the eth chain 254 // (submitting call events with malicious times could subvert, e.g. TWAPs on perp markets) 255 checkedTime, err := s.ethEngine.GetEthTime(ctx, callEvent.BlockHeight) 256 if err != nil { 257 return fmt.Errorf("unable to verify eth time at %d", callEvent.BlockHeight) 258 } 259 260 if checkedTime != callEvent.BlockTime { 261 return fmt.Errorf("call event for block time block %d alleges eth time %d - but found %d", 262 callEvent.BlockHeight, callEvent.BlockTime, checkedTime) 263 } 264 265 if callEvent.Heartbeat { 266 return s.ethConfirmations.Check(callEvent.BlockHeight) 267 } 268 269 metrics.DataSourceEthVerifierCallCounterInc(callEvent.SpecId) 270 checkResult, err := s.ethEngine.CallSpec(ctx, callEvent.SpecId, callEvent.BlockHeight) 271 if callEvent.Error != nil { 272 if err != nil { 273 if err.Error() == *callEvent.Error { 274 return nil 275 } 276 return fmt.Errorf("error mismatch, expected %s, got %s", *callEvent.Error, err.Error()) 277 } 278 279 return fmt.Errorf("call event has error %s, but no error returned from call spec", *callEvent.Error) 280 } else if err != nil { 281 return fmt.Errorf("failed to execute call event spec: %w", err) 282 } 283 284 if !bytes.Equal(callEvent.Result, checkResult.Bytes) { 285 return fmt.Errorf("mismatched results for block %d", callEvent.BlockHeight) 286 } 287 288 initialTriggerTime, err := s.ethEngine.GetInitialTriggerTime(callEvent.SpecId) 289 if err != nil { 290 return fmt.Errorf("failed to get initial trigger time: %w", err) 291 } 292 293 if callEvent.BlockTime < initialTriggerTime { 294 return fmt.Errorf("call event block time %d is before the specification's initial time %d", 295 callEvent.BlockTime, initialTriggerTime) 296 } 297 298 requiredConfirmations, err := s.ethEngine.GetRequiredConfirmations(callEvent.SpecId) 299 if err != nil { 300 return fmt.Errorf("failed to get required confirmations: %w", err) 301 } 302 303 if err = s.ethConfirmations.CheckRequiredConfirmations(callEvent.BlockHeight, requiredConfirmations); err != nil { 304 return fmt.Errorf("failed confirmations check: %w", err) 305 } 306 307 if !checkResult.PassesFilters { 308 return fmt.Errorf("failed filter check") 309 } 310 311 return nil 312 } 313 314 func (s *Verifier) removePendingCallEvent(id string) error { 315 for i, v := range s.pendingCallEvents { 316 if v.GetID() == id { 317 s.pendingCallEvents = s.pendingCallEvents[:i+copy(s.pendingCallEvents[i:], s.pendingCallEvents[i+1:])] 318 return nil 319 } 320 } 321 return fmt.Errorf("invalid pending call event hash: %s", id) 322 } 323 324 func (s *Verifier) onCallEventVerified(event interface{}, ok bool) { 325 pv, isPendingCallEvent := event.(*pendingCallEvent) 326 if !isPendingCallEvent { 327 s.log.Errorf("expected pending call event go: %T", event) 328 return 329 } 330 331 if err := s.removePendingCallEvent(pv.GetID()); err != nil { 332 s.log.Error("could not remove pending call event", logging.Error(err)) 333 } else { 334 metrics.DataSourceEthVerifierCallGaugeAdd(-1, pv.callEvent.SpecId) 335 } 336 337 if ok { 338 s.finalizedCallResults = append(s.finalizedCallResults, &pv.callEvent) 339 } else { 340 s.log.Error("failed to verify call event") 341 } 342 } 343 344 func (s *Verifier) OnTick(ctx context.Context, t time.Time) { 345 for _, callResult := range s.finalizedCallResults { 346 if s.lastBlock == nil || callResult.BlockHeight > s.lastBlock.Height { 347 s.lastBlock = &types.EthBlock{ 348 Height: callResult.BlockHeight, 349 Time: callResult.BlockTime, 350 } 351 } 352 353 if callResult.Error == nil { 354 result, err := s.ethEngine.MakeResult(callResult.SpecId, callResult.Result) 355 if err != nil { 356 s.log.Error("failed to create ethcall result", logging.Error(err)) 357 } 358 359 s.oracleEngine.BroadcastData(ctx, common.Data{ 360 EthKey: callResult.SpecId, 361 Signers: nil, 362 Data: result.Normalised, 363 MetaData: map[string]string{ 364 "eth-block-height": strconv.FormatUint(callResult.BlockHeight, 10), 365 "eth-block-time": strconv.FormatUint(callResult.BlockTime, 10), 366 "vega-time": strconv.FormatInt(t.Unix(), 10), 367 }, 368 }) 369 } else { 370 dataProto := vegapb.OracleData{ 371 ExternalData: &datapb.ExternalData{ 372 Data: &datapb.Data{ 373 MatchedSpecIds: []string{callResult.SpecId}, 374 BroadcastAt: t.UnixNano(), 375 Error: callResult.Error, 376 MetaData: []*datapb.Property{ 377 { 378 Name: "vega-time", 379 Value: strconv.FormatInt(t.Unix(), 10), 380 }, 381 }, 382 }, 383 }, 384 } 385 386 s.broker.Send(events.NewOracleDataEvent(ctx, vegapb.OracleData{ExternalData: dataProto.ExternalData})) 387 } 388 } 389 s.finalizedCallResults = nil 390 391 // keep hashes for 2 days 392 removeBefore := t.Add(-keepHashesDuration) 393 s.ackedEvts.RemoveBefore(removeBefore.Unix()) 394 }