code.vegaprotocol.io/vega@v0.79.0/core/evtforward/ethereum/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 ethereum
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"sync"
    22  	"time"
    23  
    24  	"code.vegaprotocol.io/vega/core/types"
    25  	"code.vegaprotocol.io/vega/logging"
    26  	"code.vegaprotocol.io/vega/protos/vega"
    27  	commandspb "code.vegaprotocol.io/vega/protos/vega/commands/v1"
    28  )
    29  
    30  const (
    31  	engineLogger = "engine"
    32  )
    33  
    34  var ErrInvalidHeartbeat = errors.New("forwarded heartbeat is invalid")
    35  
    36  //go:generate go run github.com/golang/mock/mockgen -destination mocks/forwarder_mock.go -package mocks code.vegaprotocol.io/vega/core/evtforward/ethereum Forwarder
    37  type Forwarder interface {
    38  	ForwardFromSelf(*commandspb.ChainEvent)
    39  }
    40  
    41  //go:generate go run github.com/golang/mock/mockgen -destination mocks/filterer_mock.go -package mocks code.vegaprotocol.io/vega/core/evtforward/ethereum Filterer
    42  type Filterer interface {
    43  	FilterCollateralEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound)
    44  	FilterStakingEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound)
    45  	FilterVestingEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound)
    46  	FilterMultisigControlEvents(ctx context.Context, startAt, stopAt uint64, cb OnEventFound)
    47  	CurrentHeight(context.Context) uint64
    48  	GetEthTime(ctx context.Context, atBlock uint64) (uint64, error)
    49  }
    50  
    51  // Contract wrapper around EthereumContract to keep track of the block heights we've checked.
    52  type Contract struct {
    53  	types.EthereumContract
    54  	next uint64 // the block height we will next check for events, all block heights less than this will have events sent in
    55  	last uint64 // the block height we last sent out an event for this contract, including heartbeats
    56  }
    57  
    58  type Engine struct {
    59  	cfg    Config
    60  	log    *logging.Logger
    61  	poller *poller
    62  
    63  	filterer  Filterer
    64  	forwarder Forwarder
    65  
    66  	chainID string
    67  
    68  	stakingDeployment    *Contract
    69  	vestingDeployment    *Contract
    70  	collateralDeployment *Contract
    71  	multisigDeployment   *Contract
    72  	mu                   sync.Mutex
    73  
    74  	cancelEthereumQueries context.CancelFunc
    75  
    76  	// the number of blocks between heartbeats
    77  	heartbeatInterval uint64
    78  }
    79  
    80  type fwdWrapper struct {
    81  	f       Forwarder
    82  	chainID string
    83  }
    84  
    85  func (f fwdWrapper) ForwardFromSelf(event *commandspb.ChainEvent) {
    86  	// add the chainID of the source on events where this is necessary
    87  	switch ev := event.Event.(type) {
    88  	case *commandspb.ChainEvent_Erc20:
    89  		ev.Erc20.ChainId = f.chainID
    90  	case *commandspb.ChainEvent_Erc20Multisig:
    91  		ev.Erc20Multisig.ChainId = f.chainID
    92  	default:
    93  		// do nothing
    94  	}
    95  
    96  	f.f.ForwardFromSelf(event)
    97  }
    98  
    99  func NewEngine(
   100  	cfg Config,
   101  	log *logging.Logger,
   102  	filterer Filterer,
   103  	forwarder Forwarder,
   104  	stakingDeployment types.EthereumContract,
   105  	vestingDeployment types.EthereumContract,
   106  	multiSigDeployment types.EthereumContract,
   107  	collateralDeployment types.EthereumContract,
   108  	chainID string,
   109  	blockTime time.Duration,
   110  ) *Engine {
   111  	l := log.Named(engineLogger)
   112  
   113  	// given that the EVM bridge configs are and array the "unset" values do not get populated
   114  	// with reasonable defaults so we need to make sure they are set to something reasonable
   115  	// if they are left out
   116  	cfg.setDefaults()
   117  
   118  	// calculate the number of blocks in an hour, this will be the interval we send out heartbeats
   119  	heartbeatTime := cfg.HeartbeatIntervalForTestOnlyDoNotChange.Duration
   120  	heartbeatInterval := heartbeatTime.Seconds() / blockTime.Seconds()
   121  
   122  	return &Engine{
   123  		cfg:                  cfg,
   124  		log:                  l,
   125  		poller:               newPoller(cfg.PollEventRetryDuration.Get()),
   126  		filterer:             filterer,
   127  		forwarder:            fwdWrapper{forwarder, chainID},
   128  		stakingDeployment:    &Contract{stakingDeployment, stakingDeployment.DeploymentBlockHeight(), stakingDeployment.DeploymentBlockHeight()},
   129  		vestingDeployment:    &Contract{vestingDeployment, vestingDeployment.DeploymentBlockHeight(), vestingDeployment.DeploymentBlockHeight()},
   130  		multisigDeployment:   &Contract{multiSigDeployment, multiSigDeployment.DeploymentBlockHeight(), multiSigDeployment.DeploymentBlockHeight()},
   131  		collateralDeployment: &Contract{collateralDeployment, collateralDeployment.DeploymentBlockHeight(), collateralDeployment.DeploymentBlockHeight()},
   132  		chainID:              chainID,
   133  		heartbeatInterval:    uint64(heartbeatInterval),
   134  	}
   135  }
   136  
   137  func (e *Engine) UpdateCollateralStartingBlock(b uint64) {
   138  	e.collateralDeployment.next = b
   139  }
   140  
   141  func (e *Engine) UpdateStakingStartingBlock(b uint64) {
   142  	e.vestingDeployment.next = b
   143  	e.stakingDeployment.next = b
   144  }
   145  
   146  func (e *Engine) UpdateMultiSigControlStartingBlock(b uint64) {
   147  	e.multisigDeployment.next = b
   148  }
   149  
   150  func (e *Engine) ReloadConf(cfg Config) {
   151  	e.log.Info("Reloading configuration")
   152  
   153  	if e.log.GetLevel() != cfg.Level.Get() {
   154  		e.log.Debug("Updating log level",
   155  			logging.String("old", e.log.GetLevel().String()),
   156  			logging.String("new", cfg.Level.String()),
   157  		)
   158  		e.log.SetLevel(cfg.Level.Get())
   159  	}
   160  }
   161  
   162  // Start starts the polling of the Ethereum bridges, listens to the events
   163  // they emit and forward it to the network.
   164  func (e *Engine) Start() {
   165  	ctx, cancelEthereumQueries := context.WithCancel(context.Background())
   166  	defer cancelEthereumQueries()
   167  
   168  	e.cancelEthereumQueries = cancelEthereumQueries
   169  	if e.log.IsDebug() {
   170  		e.log.Debug("Start listening for Ethereum events from")
   171  	}
   172  
   173  	e.poller.Loop(func() {
   174  		if e.log.IsDebug() {
   175  			e.log.Debug("Clock is ticking, gathering Ethereum events",
   176  				logging.String("chain-id", e.chainID),
   177  				logging.Uint64("next-collateral-block-number", e.collateralDeployment.next),
   178  				logging.Uint64("next-multisig-control-block-number", e.multisigDeployment.next),
   179  				logging.Uint64("next-staking-block-number", e.stakingDeployment.next),
   180  			)
   181  		}
   182  		e.gatherEvents(ctx)
   183  	})
   184  }
   185  
   186  func issueFilteringRequest(from, to, nBlocks uint64) (ok bool, actualTo uint64) {
   187  	if from > to {
   188  		return false, 0
   189  	}
   190  	return true, min(from+nBlocks, to)
   191  }
   192  
   193  func min(a, b uint64) uint64 {
   194  	if a < b {
   195  		return a
   196  	}
   197  	return b
   198  }
   199  
   200  func (e *Engine) gatherEvents(ctx context.Context) {
   201  	nBlocks := e.cfg.MaxEthereumBlocks
   202  	currentHeight := e.filterer.CurrentHeight(ctx)
   203  	e.mu.Lock()
   204  	defer e.mu.Unlock()
   205  
   206  	// Ensure we are not issuing a filtering request for non-existing block.
   207  	if ok, nextHeight := issueFilteringRequest(e.collateralDeployment.next, currentHeight, nBlocks); ok {
   208  		e.filterer.FilterCollateralEvents(ctx, e.collateralDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) {
   209  			e.forwarder.ForwardFromSelf(event)
   210  			e.collateralDeployment.last = h
   211  		})
   212  		e.collateralDeployment.next = nextHeight + 1
   213  		e.sendHeartbeat(e.collateralDeployment)
   214  	}
   215  
   216  	// Ensure we are not issuing a filtering request for non-existing block.
   217  	if e.stakingDeployment.HasAddress() {
   218  		if ok, nextHeight := issueFilteringRequest(e.stakingDeployment.next, currentHeight, nBlocks); ok {
   219  			e.filterer.FilterStakingEvents(ctx, e.stakingDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) {
   220  				e.forwarder.ForwardFromSelf(event)
   221  				e.stakingDeployment.last = h
   222  			})
   223  			e.stakingDeployment.next = nextHeight + 1
   224  			e.sendHeartbeat(e.stakingDeployment)
   225  		}
   226  	}
   227  
   228  	// Ensure we are not issuing a filtering request for non-existing block.
   229  	if e.vestingDeployment.HasAddress() {
   230  		if ok, nextHeight := issueFilteringRequest(e.vestingDeployment.next, currentHeight, nBlocks); ok {
   231  			e.filterer.FilterVestingEvents(ctx, e.vestingDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) {
   232  				e.forwarder.ForwardFromSelf(event)
   233  				e.vestingDeployment.last = h
   234  			})
   235  			e.vestingDeployment.next = nextHeight + 1
   236  			e.sendHeartbeat(e.vestingDeployment)
   237  		}
   238  	}
   239  
   240  	// Ensure we are not issuing a filtering request for non-existing block.
   241  	if ok, nextHeight := issueFilteringRequest(e.multisigDeployment.next, currentHeight, nBlocks); ok {
   242  		e.filterer.FilterMultisigControlEvents(ctx, e.multisigDeployment.next, nextHeight, func(event *commandspb.ChainEvent, h uint64) {
   243  			e.forwarder.ForwardFromSelf(event)
   244  			e.multisigDeployment.last = h
   245  		})
   246  		e.multisigDeployment.next = nextHeight + 1
   247  		e.sendHeartbeat(e.multisigDeployment)
   248  	}
   249  }
   250  
   251  // sendHeartbeat checks whether it has been more than and hour since the validator sent a chain event for the given contract
   252  // and if it has will send a heartbeat chain event so that core has an recent view on the last block checked for new events.
   253  func (e *Engine) sendHeartbeat(contract *Contract) {
   254  	// how many heartbeat intervals between the last sent event, and the block height we're checking next
   255  	n := (contract.next - contract.last) / e.heartbeatInterval
   256  	if n == 0 {
   257  		return
   258  	}
   259  
   260  	height := contract.last + n*e.heartbeatInterval
   261  	time, err := e.filterer.GetEthTime(context.Background(), height)
   262  	if err != nil {
   263  		e.log.Error("unable to find eth-time for contract heartbeat",
   264  			logging.Uint64("height", height),
   265  			logging.String("chain-id", e.chainID),
   266  			logging.Error(err),
   267  		)
   268  		return
   269  	}
   270  
   271  	e.forwarder.ForwardFromSelf(
   272  		&commandspb.ChainEvent{
   273  			TxId:  "internal", // NA
   274  			Nonce: 0,          // NA
   275  			Event: &commandspb.ChainEvent_Heartbeat{
   276  				Heartbeat: &vega.ERC20Heartbeat{
   277  					ContractAddress: contract.HexAddress(),
   278  					BlockHeight:     height,
   279  					SourceChainId:   e.chainID,
   280  					BlockTime:       time,
   281  				},
   282  			},
   283  		},
   284  	)
   285  	contract.last = height
   286  }
   287  
   288  // VerifyHeart checks that the block height of the heartbeat exists and contains the correct block time. It also
   289  // checks that this node has checked the logs of the given contract address up to at least the given height.
   290  func (e *Engine) VerifyHeartbeat(ctx context.Context, height uint64, chainID string, address string, blockTime uint64) error {
   291  	e.mu.Lock()
   292  	defer e.mu.Unlock()
   293  
   294  	t, err := e.filterer.GetEthTime(ctx, height)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	if t != blockTime {
   300  		return ErrInvalidHeartbeat
   301  	}
   302  
   303  	var lastChecked uint64
   304  	if e.collateralDeployment.HexAddress() == address {
   305  		lastChecked = e.collateralDeployment.next - 1
   306  	}
   307  
   308  	if e.multisigDeployment.HexAddress() == address {
   309  		lastChecked = e.multisigDeployment.next - 1
   310  	}
   311  
   312  	if e.stakingDeployment.HexAddress() == address {
   313  		lastChecked = e.stakingDeployment.next - 1
   314  	}
   315  
   316  	if e.vestingDeployment.HexAddress() == address {
   317  		lastChecked = e.vestingDeployment.next - 1
   318  	}
   319  
   320  	// if the heartbeat block height is higher than the last block *this* node has checked for logs
   321  	// on the contract, then fail the verification
   322  	if lastChecked < height {
   323  		return ErrInvalidHeartbeat
   324  	}
   325  	return nil
   326  }
   327  
   328  // UpdateStartingBlock sets the height that we should starting looking for new events from for the given bridge contract address.
   329  func (e *Engine) UpdateStartingBlock(address string, block uint64) {
   330  	e.mu.Lock()
   331  	defer e.mu.Unlock()
   332  
   333  	if block == 0 {
   334  		return
   335  	}
   336  
   337  	if e.collateralDeployment.HexAddress() == address {
   338  		e.collateralDeployment.last = block
   339  		e.collateralDeployment.next = block
   340  		return
   341  	}
   342  
   343  	if e.multisigDeployment.HexAddress() == address {
   344  		e.multisigDeployment.last = block
   345  		e.multisigDeployment.next = block
   346  		return
   347  	}
   348  
   349  	if e.stakingDeployment.HexAddress() == address {
   350  		e.stakingDeployment.last = block
   351  		e.stakingDeployment.next = block
   352  		return
   353  	}
   354  
   355  	if e.vestingDeployment.HexAddress() == address {
   356  		e.vestingDeployment.last = block
   357  		e.vestingDeployment.next = block
   358  		return
   359  	}
   360  
   361  	e.log.Warn("unexpected contract address starting block",
   362  		logging.String("chain-id", e.chainID),
   363  		logging.String("contract-address", address),
   364  	)
   365  }
   366  
   367  // Stop stops the engine, its polling and event forwarding.
   368  func (e *Engine) Stop() {
   369  	// Notify to stop on next iteration.
   370  	e.poller.Stop()
   371  	// Cancel any ongoing queries against Ethereum.
   372  	if e.cancelEthereumQueries != nil {
   373  		e.cancelEthereumQueries()
   374  	}
   375  }
   376  
   377  // poller wraps a poller that ticks every durationBetweenTwoEventFiltering.
   378  type poller struct {
   379  	ticker                  *time.Ticker
   380  	done                    chan bool
   381  	durationBetweenTwoRetry time.Duration
   382  }
   383  
   384  func newPoller(durationBetweenTwoRetry time.Duration) *poller {
   385  	return &poller{
   386  		ticker:                  time.NewTicker(durationBetweenTwoRetry),
   387  		done:                    make(chan bool, 1),
   388  		durationBetweenTwoRetry: durationBetweenTwoRetry,
   389  	}
   390  }
   391  
   392  // Loop starts the poller loop until it's broken, using the Stop method.
   393  func (s *poller) Loop(fn func()) {
   394  	defer func() {
   395  		s.ticker.Stop()
   396  		s.ticker.Reset(s.durationBetweenTwoRetry)
   397  	}()
   398  
   399  	for {
   400  		select {
   401  		case <-s.done:
   402  			return
   403  		case <-s.ticker.C:
   404  			fn()
   405  		}
   406  	}
   407  }
   408  
   409  // Stop stops the poller loop.
   410  func (s *poller) Stop() {
   411  	s.done <- true
   412  }