github.com/smartcontractkit/chainlink-testing-framework/libs@v0.0.0-20240227141906-ec710b4eb1a3/blockchain/finalized_headers.go (about)

     1  package blockchain
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"math/big"
     7  	"sync"
     8  	"sync/atomic"
     9  	"time"
    10  
    11  	"github.com/ethereum/go-ethereum/common"
    12  	"github.com/rs/zerolog"
    13  	"github.com/rs/zerolog/log"
    14  )
    15  
    16  const (
    17  	minBlocksToFinalize = 10
    18  	finalityTimeout     = 45 * time.Minute
    19  	FinalizedHeaderKey  = "finalizedHeads"
    20  	logNotifyFrequency  = 2 * time.Minute
    21  )
    22  
    23  var (
    24  	globalFinalizedHeaderManager = sync.Map{}
    25  )
    26  
    27  // FinalizedHeader is an implementation of the HeaderEventSubscription interface.
    28  // It keeps track of the latest finalized header for a network.
    29  type FinalizedHeader struct {
    30  	lggr              zerolog.Logger
    31  	LatestFinalized   atomic.Value // *big.Int
    32  	FinalizedAt       atomic.Value // time.Time
    33  	client            EVMClient
    34  	headerUpdateMutex *sync.Mutex
    35  }
    36  
    37  // Wait is not a blocking call.
    38  func (f *FinalizedHeader) Wait() error {
    39  	return nil
    40  }
    41  
    42  // Complete returns false as the HeaderEventSubscription should run for the entire course of the test.
    43  func (f *FinalizedHeader) Complete() bool {
    44  	return false
    45  }
    46  
    47  // ReceiveHeader is called whenever a new header is received.
    48  // During the course of test whenever a new header is received, ReceiveHeader checks if there is a new finalized header tagged.
    49  func (f *FinalizedHeader) ReceiveHeader(header NodeHeader) error {
    50  	fLatest := f.LatestFinalized.Load().(*big.Int)
    51  	// assumption : it will take at least minBlocksToFinalize num of blocks to finalize
    52  	// this is to reduce the number of calls to the client
    53  	if new(big.Int).Sub(header.Number, fLatest).Cmp(big.NewInt(minBlocksToFinalize)) <= 0 {
    54  		return nil
    55  	}
    56  	f.headerUpdateMutex.Lock()
    57  	defer f.headerUpdateMutex.Unlock()
    58  	if f.FinalizedAt.Load() != nil {
    59  		fTime := f.FinalizedAt.Load().(time.Time)
    60  		// if the time difference between the new header and the last finalized header is less than 10s, ignore
    61  		if header.Timestamp.Sub(fTime) <= 10*time.Second {
    62  			return nil
    63  		}
    64  	}
    65  
    66  	ctx, ctxCancel := context.WithTimeout(context.Background(), f.client.GetNetworkConfig().Timeout.Duration)
    67  	lastFinalized, err := f.client.GetLatestFinalizedBlockHeader(ctx)
    68  	ctxCancel()
    69  	if err != nil {
    70  		return fmt.Errorf("error getting latest finalized block header: %w", err)
    71  	}
    72  
    73  	if lastFinalized.Number.Cmp(fLatest) > 0 {
    74  		f.LatestFinalized.Store(lastFinalized.Number)
    75  		f.FinalizedAt.Store(header.Timestamp)
    76  		f.lggr.Info().
    77  			Str("Finalized Header", lastFinalized.Number.String()).
    78  			Str("Finalized At", header.Timestamp.String()).
    79  			Msg("new finalized header received")
    80  	}
    81  	return nil
    82  }
    83  
    84  // newGlobalFinalizedHeaderManager is a global manager for finalized headers per network.
    85  // It is used to keep track of the latest finalized header for each network.
    86  func newGlobalFinalizedHeaderManager(evmClient EVMClient) *FinalizedHeader {
    87  	// if simulated network or finality depth is greater than 0, there is no need to track finalized headers return nil
    88  	if evmClient.NetworkSimulated() || evmClient.GetNetworkConfig().FinalityDepth > 0 {
    89  		return nil
    90  	}
    91  	f, ok := globalFinalizedHeaderManager.Load(evmClient.GetChainID().String())
    92  	isFinalizedHeaderObsolete := false
    93  	var fHeader *FinalizedHeader
    94  	if f != nil {
    95  		now := time.Now().UTC()
    96  		// if the last finalized header is older than an hour
    97  		lastFinalizedAt := f.(*FinalizedHeader).FinalizedAt.Load().(time.Time)
    98  		isFinalizedHeaderObsolete = now.Sub(lastFinalizedAt) > 1*time.Hour
    99  		fHeader = f.(*FinalizedHeader)
   100  	}
   101  
   102  	// if there is no finalized header for this network or the last finalized header is older than 1 hour
   103  	if !ok || isFinalizedHeaderObsolete {
   104  		mu := &sync.Mutex{}
   105  		mu.Lock()
   106  		defer mu.Unlock()
   107  		ctx, ctxCancel := context.WithTimeout(context.Background(), evmClient.GetNetworkConfig().Timeout.Duration)
   108  		lastFinalized, err := evmClient.GetLatestFinalizedBlockHeader(ctx)
   109  		ctxCancel()
   110  		if err != nil {
   111  			log.Err(fmt.Errorf("error getting latest finalized block header")).Msg("NewFinalizedHeader")
   112  			return nil
   113  		}
   114  		fHeader = &FinalizedHeader{
   115  			lggr:              log.With().Str("Network", evmClient.GetNetworkName()).Logger(),
   116  			client:            evmClient,
   117  			headerUpdateMutex: mu,
   118  		}
   119  		fHeader.LatestFinalized.Store(lastFinalized.Number)
   120  		fHeader.FinalizedAt.Store(time.Now().UTC())
   121  		globalFinalizedHeaderManager.Store(evmClient.GetChainID().String(), fHeader)
   122  		fHeader.lggr.Info().
   123  			Str("Finalized Header", lastFinalized.Number.String()).
   124  			Str("Finalized At", time.Now().UTC().String()).
   125  			Msg("new finalized header received")
   126  	}
   127  
   128  	return fHeader
   129  }
   130  
   131  // TransactionFinalizer is an implementation of HeaderEventSubscription that waits for a transaction to be finalized.
   132  type TransactionFinalizer struct {
   133  	lggr          zerolog.Logger
   134  	client        EVMClient
   135  	doneChan      chan struct{}
   136  	context       context.Context
   137  	cancel        context.CancelFunc
   138  	networkConfig *EVMNetwork
   139  	complete      bool
   140  	completeMu    sync.Mutex
   141  	txHdr         *SafeEVMHeader
   142  	txHash        common.Hash
   143  	FinalizedBy   *big.Int
   144  	FinalizedAt   time.Time
   145  	lastLogUpdate time.Time
   146  }
   147  
   148  func NewTransactionFinalizer(client EVMClient, txHdr *SafeEVMHeader, txHash common.Hash) *TransactionFinalizer {
   149  	ctx, ctxCancel := context.WithTimeout(context.Background(), finalityTimeout)
   150  	tf := &TransactionFinalizer{
   151  		lggr: log.With().
   152  			Str("txHash", txHash.String()).
   153  			Str("Tx Block", txHdr.Number.String()).
   154  			Str("Network", client.GetNetworkName()).
   155  			Logger(),
   156  		client:        client,
   157  		doneChan:      make(chan struct{}, 1),
   158  		context:       ctx,
   159  		cancel:        ctxCancel,
   160  		networkConfig: client.GetNetworkConfig(),
   161  		complete:      false,
   162  		txHdr:         txHdr,
   163  		txHash:        txHash,
   164  	}
   165  
   166  	return tf
   167  }
   168  
   169  func (tf *TransactionFinalizer) ReceiveHeader(header NodeHeader) error {
   170  	// if simulated network, return
   171  	if tf.client.NetworkSimulated() {
   172  		return nil
   173  	}
   174  	isFinalized, by, at, err := tf.client.IsTxHeadFinalized(tf.txHdr, &header.SafeEVMHeader)
   175  	if err != nil {
   176  		return err
   177  	}
   178  	if isFinalized {
   179  		tf.lggr.Info().
   180  			Str("Finalized Block", header.Number.String()).
   181  			Str("Tx block", tf.txHdr.Number.String()).
   182  			Msg("Found finalized log")
   183  		tf.complete = true
   184  		tf.doneChan <- struct{}{}
   185  		tf.FinalizedBy = by
   186  		tf.FinalizedAt = at
   187  	} else {
   188  		lgEvent := tf.lggr.Info()
   189  		// if the transaction is not finalized, notify every logNotifyFrequency duration
   190  		if time.Now().UTC().Sub(tf.lastLogUpdate) < logNotifyFrequency {
   191  			return nil
   192  		}
   193  		if tf.networkConfig.FinalityDepth > 0 {
   194  			lgEvent.
   195  				Str("Current Block", header.Number.String()).
   196  				Uint64("Finality Depth", tf.networkConfig.FinalityDepth)
   197  		} else {
   198  			lgEvent.
   199  				Str("Last Finalized Block", by.String())
   200  		}
   201  		lgEvent.Msg("Still Waiting for transaction log to be finalized")
   202  		tf.lastLogUpdate = time.Now().UTC()
   203  	}
   204  	return nil
   205  }
   206  
   207  // Wait is a blocking function that waits until the transaction is finalized or the context is cancelled
   208  func (tf *TransactionFinalizer) Wait() error {
   209  	defer func() {
   210  		tf.completeMu.Lock()
   211  		tf.complete = true
   212  		tf.completeMu.Unlock()
   213  	}()
   214  
   215  	if tf.Complete() {
   216  		tf.cancel()
   217  		return nil
   218  	}
   219  
   220  	for {
   221  		select {
   222  		case <-tf.doneChan:
   223  			tf.cancel()
   224  			return nil
   225  		case <-tf.context.Done():
   226  			return fmt.Errorf("timeout waiting for transaction to be finalized: %s network %s", tf.txHash, tf.client.GetNetworkName())
   227  		}
   228  	}
   229  }
   230  
   231  // Complete returns if the finalizer has completed or not
   232  func (tf *TransactionFinalizer) Complete() bool {
   233  	tf.completeMu.Lock()
   234  	defer tf.completeMu.Unlock()
   235  	return tf.complete
   236  }