code.vegaprotocol.io/vega@v0.79.0/core/client/eth/ethereum_confirmations.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 eth
    17  
    18  import (
    19  	"context"
    20  	"errors"
    21  	"math/big"
    22  	"sync"
    23  	"time"
    24  
    25  	ethtypes "github.com/ethereum/go-ethereum/core/types"
    26  )
    27  
    28  var (
    29  	ErrMissingConfirmations = errors.New("not enough confirmations")
    30  	ErrBlockNotFinalized    = errors.New("block not finalized")
    31  )
    32  
    33  type FinalityState int
    34  
    35  const (
    36  	FinalityStateSafe FinalityState = iota
    37  	FinalityStateFinalized
    38  	FinalityStateLatest
    39  )
    40  
    41  //go:generate go run github.com/golang/mock/mockgen -destination mocks/ethereum_client_confirmations_mock.go -package mocks code.vegaprotocol.io/vega/core/staking EthereumClientConfirmations
    42  type EthereumClientConfirmations interface {
    43  	HeaderByNumber(context.Context, *big.Int) (*ethtypes.Header, error)
    44  }
    45  
    46  //go:generate go run github.com/golang/mock/mockgen -destination mocks/time_mock.go -package mocks code.vegaprotocol.io/vega/core/client/eth Time
    47  type Time interface {
    48  	Now() time.Time
    49  }
    50  
    51  type StdTime struct{}
    52  
    53  func (StdTime) Now() time.Time { return time.Now() }
    54  
    55  type EthereumConfirmations struct {
    56  	retryDelay time.Duration
    57  
    58  	ethClient EthereumClientConfirmations
    59  
    60  	time Time
    61  
    62  	mu                  sync.Mutex
    63  	required            uint64
    64  	curHeight           uint64
    65  	curHeightLastUpdate time.Time
    66  	finHeight           uint64
    67  	finHeightLastUpdate time.Time
    68  	finState            *big.Int
    69  }
    70  
    71  func NewEthereumConfirmations(cfg Config, ethClient EthereumClientConfirmations, time Time, cs FinalityState) *EthereumConfirmations {
    72  	if time == nil {
    73  		time = StdTime{}
    74  	}
    75  
    76  	conf := &EthereumConfirmations{
    77  		retryDelay: cfg.RetryDelay.Get(),
    78  		ethClient:  ethClient,
    79  		time:       time,
    80  	}
    81  
    82  	switch cs {
    83  	case FinalityStateSafe:
    84  		conf.finState = big.NewInt(-4)
    85  	case FinalityStateFinalized:
    86  		conf.finState = big.NewInt(-3)
    87  	case FinalityStateLatest:
    88  		conf.finState = nil
    89  	default:
    90  		panic("unexpected confirmation state")
    91  	}
    92  	return conf
    93  }
    94  
    95  func (e *EthereumConfirmations) GetConfirmations() uint64 {
    96  	e.mu.Lock()
    97  	defer e.mu.Unlock()
    98  	return e.required
    99  }
   100  
   101  func (e *EthereumConfirmations) UpdateConfirmations(confirmations uint64) {
   102  	e.mu.Lock()
   103  	defer e.mu.Unlock()
   104  	e.required = confirmations
   105  }
   106  
   107  func (e *EthereumConfirmations) Check(block uint64) error {
   108  	if err := e.CheckRequiredConfirmations(block, e.required); err != nil {
   109  		return err
   110  	}
   111  
   112  	// if finality state is "latest" we do not need to check as this will already be done by the confirmations count
   113  	if e.finState == nil {
   114  		return nil
   115  	}
   116  
   117  	finalized, err := e.finalizedHeight(context.Background())
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	if finalized < block {
   123  		return ErrBlockNotFinalized
   124  	}
   125  
   126  	return nil
   127  }
   128  
   129  func (e *EthereumConfirmations) CheckRequiredConfirmations(block uint64, required uint64) error {
   130  	curBlock, err := e.currentHeight(context.Background())
   131  	if err != nil {
   132  		return err
   133  	}
   134  
   135  	if curBlock < block || (curBlock-block) < required {
   136  		return ErrMissingConfirmations
   137  	}
   138  
   139  	return nil
   140  }
   141  
   142  func (e *EthereumConfirmations) finalizedHeight(ctx context.Context) (uint64, error) {
   143  	e.mu.Lock()
   144  	defer e.mu.Unlock()
   145  
   146  	h, lastUpdate, err := e.getHeight(ctx, e.finHeight, e.finHeightLastUpdate, e.finState)
   147  	if err != nil {
   148  		return e.finHeight, err
   149  	}
   150  
   151  	// update cache
   152  	e.finHeightLastUpdate = lastUpdate
   153  	e.finHeight = h
   154  	return e.finHeight, err
   155  }
   156  
   157  func (e *EthereumConfirmations) currentHeight(ctx context.Context) (uint64, error) {
   158  	e.mu.Lock()
   159  	defer e.mu.Unlock()
   160  
   161  	h, lastUpdate, err := e.getHeight(ctx, e.curHeight, e.curHeightLastUpdate, nil)
   162  	if err != nil {
   163  		return e.curHeight, err
   164  	}
   165  
   166  	// update cache
   167  	e.curHeightLastUpdate = lastUpdate
   168  	e.curHeight = h
   169  	return e.curHeight, err
   170  }
   171  
   172  func (e *EthereumConfirmations) getHeight(ctx context.Context, lastHeight uint64, lastUpdate time.Time, block *big.Int) (uint64, time.Time, error) {
   173  	// if last update of the height was more that 15 seconds
   174  	// ago, we try to update, we assume an eth block takes
   175  	// ~15 seconds
   176  	ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
   177  	defer cancel()
   178  	if now := e.time.Now(); lastUpdate.Add(e.retryDelay).Before(now) {
   179  		// get the last block header
   180  		h, err := e.ethClient.HeaderByNumber(ctx, block)
   181  		if err != nil {
   182  			return lastHeight, lastUpdate, err
   183  		}
   184  		lastUpdate = now
   185  		lastHeight = h.Number.Uint64()
   186  	}
   187  
   188  	return lastHeight, lastUpdate, nil
   189  }