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  }