code.vegaprotocol.io/vega@v0.79.0/core/datasource/external/ethcall/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 ethcall
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"log"
    22  	"math/big"
    23  	"reflect"
    24  	"strconv"
    25  	"sync"
    26  	"sync/atomic"
    27  	"time"
    28  
    29  	"code.vegaprotocol.io/vega/core/datasource"
    30  	"code.vegaprotocol.io/vega/core/datasource/external/ethcall/common"
    31  	"code.vegaprotocol.io/vega/libs/ptr"
    32  	"code.vegaprotocol.io/vega/logging"
    33  	"code.vegaprotocol.io/vega/protos/vega"
    34  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    35  
    36  	"github.com/ethereum/go-ethereum"
    37  )
    38  
    39  type EthReaderCaller interface {
    40  	ethereum.ContractCaller
    41  	ethereum.ChainReader
    42  	ChainID(context.Context) (*big.Int, error)
    43  }
    44  
    45  //go:generate go run github.com/golang/mock/mockgen -destination mocks/forwarder_mock.go -package mocks code.vegaprotocol.io/vega/core/datasource/external/ethcall Forwarder
    46  type Forwarder interface {
    47  	ForwardFromSelf(*commandspb.ChainEvent)
    48  }
    49  
    50  type blockish interface {
    51  	NumberU64() uint64
    52  	Time() uint64
    53  }
    54  
    55  type blockIndex struct {
    56  	number uint64
    57  	time   uint64
    58  }
    59  
    60  func (b blockIndex) NumberU64() uint64 {
    61  	return b.number
    62  }
    63  
    64  func (b blockIndex) Time() uint64 {
    65  	return b.time
    66  }
    67  
    68  type Engine struct {
    69  	log                   *logging.Logger
    70  	cfg                   Config
    71  	isValidator           bool
    72  	client                EthReaderCaller
    73  	calls                 map[string]Call
    74  	forwarder             Forwarder
    75  	prevEthBlock          blockish
    76  	cancelEthereumQueries context.CancelFunc
    77  	poller                *poller
    78  	mu                    sync.Mutex
    79  
    80  	chainID           atomic.Uint64
    81  	blockInterval     atomic.Uint64
    82  	lastSent          blockish
    83  	heartbeatInterval time.Duration
    84  }
    85  
    86  func NewEngine(log *logging.Logger, cfg Config, isValidator bool, client EthReaderCaller, forwarder Forwarder) *Engine {
    87  	e := &Engine{
    88  		log:               log,
    89  		cfg:               cfg,
    90  		isValidator:       isValidator,
    91  		client:            client,
    92  		forwarder:         forwarder,
    93  		calls:             make(map[string]Call),
    94  		poller:            newPoller(cfg.PollEvery.Get()),
    95  		heartbeatInterval: cfg.HeartbeatIntervalForTestOnlyDoNotChange.Get(),
    96  	}
    97  
    98  	// default to 1 block interval
    99  	e.blockInterval.Store(1)
   100  
   101  	return e
   102  }
   103  
   104  // EnsureChainID tells the engine which chainID it should be related to, and it confirms this against the its client.
   105  func (e *Engine) EnsureChainID(ctx context.Context, chainID string, blockInterval uint64, confirmWithClient bool) {
   106  	chainIDU, _ := strconv.ParseUint(chainID, 10, 64)
   107  	e.chainID.Store(chainIDU)
   108  	e.blockInterval.Store(blockInterval)
   109  	// cover backward compatibility for L2
   110  	if e.blockInterval.Load() == 0 {
   111  		e.blockInterval.Store(1)
   112  	}
   113  
   114  	// if the node is a validator, we now check the chainID against the chain the client is connected to.
   115  	if confirmWithClient {
   116  		cid, err := e.client.ChainID(ctx)
   117  		if err != nil {
   118  			log.Panic("could not load chain ID", logging.Error(err))
   119  		}
   120  
   121  		if cid.Uint64() != e.chainID.Load() {
   122  			log.Panic("chain ID mismatch between ethCall engine and EVM client",
   123  				logging.Uint64("client-chain-id", cid.Uint64()),
   124  				logging.Uint64("engine-chain-id", e.chainID.Load()),
   125  			)
   126  		}
   127  	}
   128  }
   129  
   130  // Start starts the polling of the Ethereum bridges, listens to the events
   131  // they emit and forward it to the network.
   132  func (e *Engine) Start() {
   133  	if e.isValidator && !reflect.ValueOf(e.client).IsNil() {
   134  		go func() {
   135  			ctx, cancelEthereumQueries := context.WithCancel(context.Background())
   136  			defer cancelEthereumQueries()
   137  
   138  			e.cancelEthereumQueries = cancelEthereumQueries
   139  			e.log.Info("Starting ethereum contract call polling engine", logging.Uint64("chain-id", e.chainID.Load()))
   140  
   141  			e.poller.Loop(func() {
   142  				e.Poll(ctx, time.Now())
   143  			})
   144  		}()
   145  	}
   146  }
   147  
   148  func (e *Engine) StartAtHeight(height uint64, time uint64) {
   149  	e.prevEthBlock = blockIndex{number: height, time: time}
   150  	e.lastSent = blockIndex{number: height, time: time}
   151  	e.Start()
   152  }
   153  
   154  func (e *Engine) getCalls() map[string]Call {
   155  	e.mu.Lock()
   156  	defer e.mu.Unlock()
   157  	calls := map[string]Call{}
   158  	for specID, call := range e.calls {
   159  		calls[specID] = call
   160  	}
   161  	return calls
   162  }
   163  
   164  func (e *Engine) Stop() {
   165  	// Notify to stop on next iteration.
   166  	e.poller.Stop()
   167  	// Cancel any ongoing queries against Ethereum.
   168  	if e.cancelEthereumQueries != nil {
   169  		e.cancelEthereumQueries()
   170  	}
   171  }
   172  
   173  func (e *Engine) GetSpec(id string) (common.Spec, bool) {
   174  	e.mu.Lock()
   175  	defer e.mu.Unlock()
   176  	if source, ok := e.calls[id]; ok {
   177  		return source.spec, true
   178  	}
   179  
   180  	return common.Spec{}, false
   181  }
   182  
   183  func (e *Engine) MakeResult(specID string, bytes []byte) (Result, error) {
   184  	e.mu.Lock()
   185  	defer e.mu.Unlock()
   186  	call, ok := e.calls[specID]
   187  	if !ok {
   188  		return Result{}, fmt.Errorf("no such specification: %v", specID)
   189  	}
   190  	return newResult(call, bytes)
   191  }
   192  
   193  func (e *Engine) CallSpec(ctx context.Context, id string, atBlock uint64) (Result, error) {
   194  	e.mu.Lock()
   195  	call, ok := e.calls[id]
   196  	if !ok {
   197  		e.mu.Unlock()
   198  		return Result{}, fmt.Errorf("no such specification: %v", id)
   199  	}
   200  	e.mu.Unlock()
   201  
   202  	return call.Call(ctx, e.client, atBlock)
   203  }
   204  
   205  func (e *Engine) GetEthTime(ctx context.Context, atBlock uint64) (uint64, error) {
   206  	blockNum := big.NewInt(0).SetUint64(atBlock)
   207  	header, err := e.client.HeaderByNumber(ctx, blockNum)
   208  	if err != nil {
   209  		return 0, fmt.Errorf("failed to get block header: %w", err)
   210  	}
   211  
   212  	if header == nil {
   213  		return 0, fmt.Errorf("nil block header: %w", err)
   214  	}
   215  
   216  	return header.Time, nil
   217  }
   218  
   219  func (e *Engine) GetRequiredConfirmations(id string) (uint64, error) {
   220  	e.mu.Lock()
   221  	call, ok := e.calls[id]
   222  	if !ok {
   223  		e.mu.Unlock()
   224  		return 0, fmt.Errorf("no such specification: %v", id)
   225  	}
   226  	e.mu.Unlock()
   227  
   228  	return call.spec.RequiredConfirmations, nil
   229  }
   230  
   231  func (e *Engine) GetInitialTriggerTime(id string) (uint64, error) {
   232  	e.mu.Lock()
   233  	call, ok := e.calls[id]
   234  	if !ok {
   235  		e.mu.Unlock()
   236  		return 0, fmt.Errorf("no such specification: %v", id)
   237  	}
   238  	e.mu.Unlock()
   239  
   240  	return call.initialTime(), nil
   241  }
   242  
   243  func (e *Engine) OnSpecActivated(ctx context.Context, spec datasource.Spec) error {
   244  	e.mu.Lock()
   245  	defer e.mu.Unlock()
   246  	switch d := spec.Data.Content().(type) {
   247  	case common.Spec:
   248  		id := spec.ID
   249  		if _, ok := e.calls[id]; ok {
   250  			return fmt.Errorf("duplicate spec: %s", id)
   251  		}
   252  
   253  		ethCall, err := NewCall(d)
   254  		if err != nil {
   255  			return fmt.Errorf("failed to create data source: %w", err)
   256  		}
   257  
   258  		// here ensure we are on the engine with the right network ID
   259  		// not an error, just return
   260  		if e.chainID.Load() != d.SourceChainID {
   261  			return nil
   262  		}
   263  
   264  		e.calls[id] = ethCall
   265  	}
   266  
   267  	return nil
   268  }
   269  
   270  func (e *Engine) OnSpecDeactivated(ctx context.Context, spec datasource.Spec) {
   271  	e.mu.Lock()
   272  	defer e.mu.Unlock()
   273  	switch spec.Data.Content().(type) {
   274  	case common.Spec:
   275  		id := spec.ID
   276  		delete(e.calls, id)
   277  	}
   278  }
   279  
   280  // Poll is called by the poller in it's own goroutine; it isn't part of the abci code path.
   281  func (e *Engine) Poll(ctx context.Context, wallTime time.Time) {
   282  	// Don't take the mutex here to avoid blocking abci engine while doing potentially lengthy ethereum calls
   283  	// Instead call methods on the engine that take the mutex for a small time where needed.
   284  	// We do need to make use direct use of of e.log, e.client and e.forwarder; but these are static after creation
   285  	// and the methods used are safe for concurrent access.
   286  	lastEthBlock, err := e.client.HeaderByNumber(ctx, nil)
   287  	if err != nil {
   288  		e.log.Error("failed to get current block header", logging.Error(err))
   289  		return
   290  	}
   291  
   292  	e.log.Info("tick",
   293  		logging.Uint64("chainID", e.chainID.Load()),
   294  		logging.Time("wallTime", wallTime),
   295  		logging.BigInt("ethBlock", lastEthBlock.Number),
   296  		logging.Time("ethTime", time.Unix(int64(lastEthBlock.Time), 0)))
   297  
   298  	// If the previous eth block has not been set, set it to the current eth block
   299  	if e.prevEthBlock == nil {
   300  		e.prevEthBlock = blockIndex{number: lastEthBlock.Number.Uint64(), time: lastEthBlock.Time}
   301  		e.lastSent = blockIndex{number: lastEthBlock.Number.Uint64(), time: lastEthBlock.Time}
   302  	}
   303  
   304  	// Go through an eth blocks one at a time until we get to the most recent one
   305  	for prevEthBlock := e.prevEthBlock; prevEthBlock.NumberU64() < lastEthBlock.Number.Uint64(); prevEthBlock = e.prevEthBlock {
   306  		nextBlockNum := big.NewInt(0).SetUint64(prevEthBlock.NumberU64() + e.blockInterval.Load())
   307  		nextEthBlock, err := e.client.HeaderByNumber(ctx, nextBlockNum)
   308  		if err != nil {
   309  			e.log.Error("failed to get next block header",
   310  				logging.Error(err),
   311  				logging.Uint64("chain-id", e.chainID.Load()),
   312  				logging.Uint64("prev-block", prevEthBlock.NumberU64()),
   313  				logging.Uint64("last-block", lastEthBlock.Number.Uint64()),
   314  				logging.Uint64("expect-next-block", nextBlockNum.Uint64()),
   315  			)
   316  			return
   317  		}
   318  
   319  		nextEthBlockIsh := blockIndex{number: nextEthBlock.Number.Uint64(), time: nextEthBlock.Time}
   320  		for specID, call := range e.getCalls() {
   321  			if call.triggered(prevEthBlock, nextEthBlockIsh) {
   322  				res, err := call.Call(ctx, e.client, nextEthBlock.Number.Uint64())
   323  				if err != nil {
   324  					e.log.Error("failed to call contract", logging.Error(err), logging.String("spec-id", specID), logging.Uint64("chain-id", e.chainID.Load()))
   325  					event := makeErrorChainEvent(err.Error(), specID, nextEthBlockIsh, e.chainID.Load())
   326  					e.forwarder.ForwardFromSelf(event)
   327  					e.lastSent = nextEthBlockIsh
   328  					continue
   329  				}
   330  
   331  				if res.PassesFilters {
   332  					event := makeChainEvent(res, specID, nextEthBlockIsh, e.chainID.Load())
   333  					e.forwarder.ForwardFromSelf(event)
   334  					e.lastSent = nextEthBlockIsh
   335  				}
   336  			}
   337  		}
   338  
   339  		if e.sendHeartbeat(nextEthBlockIsh) {
   340  			// we've not forwarded an ethcall result for a while, send a dummy heartbeat
   341  			event := makeHeartbeat(nextEthBlockIsh, e.chainID.Load())
   342  			e.forwarder.ForwardFromSelf(event)
   343  			e.lastSent = nextEthBlockIsh
   344  		}
   345  
   346  		e.prevEthBlock = nextEthBlockIsh
   347  	}
   348  }
   349  
   350  // sendHeartbeat returns true if the difference in block time between the current eth block and the last sent even is
   351  // above a given threshold.
   352  func (e *Engine) sendHeartbeat(block blockish) bool {
   353  	now := time.Unix(int64(block.Time()), 0)
   354  	last := time.Unix(int64(e.lastSent.Time()), 0)
   355  	return last.Add(e.heartbeatInterval).Before(now)
   356  }
   357  
   358  func makeHeartbeat(block blockish, chainID uint64) *commandspb.ChainEvent {
   359  	ce := commandspb.ChainEvent{
   360  		TxId:  "internal", // NA
   361  		Nonce: 0,          // NA
   362  		Event: &commandspb.ChainEvent_ContractCall{
   363  			ContractCall: &vega.EthContractCallEvent{
   364  				BlockHeight:   block.NumberU64(),
   365  				BlockTime:     block.Time(),
   366  				SourceChainId: ptr.From(chainID),
   367  				Heartbeat:     true,
   368  			},
   369  		},
   370  	}
   371  
   372  	return &ce
   373  }
   374  
   375  func makeChainEvent(res Result, specID string, block blockish, chainID uint64) *commandspb.ChainEvent {
   376  	ce := commandspb.ChainEvent{
   377  		TxId:  "internal", // NA
   378  		Nonce: 0,          // NA
   379  		Event: &commandspb.ChainEvent_ContractCall{
   380  			ContractCall: &vega.EthContractCallEvent{
   381  				SpecId:        specID,
   382  				BlockHeight:   block.NumberU64(),
   383  				BlockTime:     block.Time(),
   384  				Result:        res.Bytes,
   385  				SourceChainId: ptr.From(chainID),
   386  				Heartbeat:     false,
   387  			},
   388  		},
   389  	}
   390  
   391  	return &ce
   392  }
   393  
   394  func makeErrorChainEvent(errMsg string, specID string, block blockish, chainID uint64) *commandspb.ChainEvent {
   395  	ce := commandspb.ChainEvent{
   396  		TxId:  "internal", // NA
   397  		Nonce: 0,          // NA
   398  		Event: &commandspb.ChainEvent_ContractCall{
   399  			ContractCall: &vega.EthContractCallEvent{
   400  				SpecId:        specID,
   401  				BlockHeight:   block.NumberU64(),
   402  				BlockTime:     block.Time(),
   403  				Error:         &errMsg,
   404  				SourceChainId: ptr.From(chainID),
   405  			},
   406  		},
   407  	}
   408  
   409  	return &ce
   410  }
   411  
   412  func (e *Engine) ReloadConf(cfg Config) {
   413  	e.log.Info("Reloading configuration")
   414  
   415  	if e.log.GetLevel() != cfg.Level.Get() {
   416  		e.log.Debug("Updating log level",
   417  			logging.String("old", e.log.GetLevel().String()),
   418  			logging.String("new", cfg.Level.String()),
   419  		)
   420  		e.log.SetLevel(cfg.Level.Get())
   421  	}
   422  }
   423  
   424  // This is copy-pasted from the ethereum engine; at some point this two should probably be folded into one,
   425  // but just for now keep them separate to ensure we don't break existing functionality.
   426  type poller struct {
   427  	done      chan bool
   428  	pollEvery time.Duration
   429  }
   430  
   431  func newPoller(pollEvery time.Duration) *poller {
   432  	return &poller{
   433  		done:      make(chan bool, 1),
   434  		pollEvery: pollEvery,
   435  	}
   436  }
   437  
   438  // Loop starts the poller loop until it's broken, using the Stop method.
   439  func (s *poller) Loop(fn func()) {
   440  	ticker := time.NewTicker(s.pollEvery)
   441  	defer func() {
   442  		ticker.Stop()
   443  		ticker.Reset(s.pollEvery)
   444  	}()
   445  
   446  	for {
   447  		select {
   448  		case <-s.done:
   449  			return
   450  		case <-ticker.C:
   451  			fn()
   452  		}
   453  	}
   454  }
   455  
   456  // Stop stops the poller loop.
   457  func (s *poller) Stop() {
   458  	s.done <- true
   459  }