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 }