github.com/ethersphere/bee/v2@v2.2.0/pkg/transaction/monitor.go (about)

     1  // Copyright 2021 The Swarm Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package transaction
     6  
     7  import (
     8  	"context"
     9  	"errors"
    10  	"io"
    11  	"math/big"
    12  	"sync"
    13  	"time"
    14  
    15  	"github.com/ethereum/go-ethereum"
    16  	"github.com/ethereum/go-ethereum/common"
    17  	"github.com/ethereum/go-ethereum/core/types"
    18  	"github.com/ethersphere/bee/v2/pkg/log"
    19  )
    20  
    21  var ErrTransactionCancelled = errors.New("transaction cancelled")
    22  var ErrMonitorClosed = errors.New("monitor closed")
    23  
    24  // Monitor is a nonce-based watcher for transaction confirmations.
    25  // Instead of watching transactions individually, the senders nonce is monitored and transactions are checked based on this.
    26  // The idea is that if the nonce is still lower than that of a pending transaction, there is no point in actually checking the transaction for a receipt.
    27  // At the same time if the nonce was already used and this was a few blocks ago we can reasonably assume that it will never confirm.
    28  type Monitor interface {
    29  	io.Closer
    30  	// WatchTransaction watches the transaction until either there is 1 confirmation or a competing transaction with cancellationDepth confirmations.
    31  	WatchTransaction(txHash common.Hash, nonce uint64) (<-chan types.Receipt, <-chan error, error)
    32  }
    33  type transactionMonitor struct {
    34  	lock       sync.Mutex
    35  	ctx        context.Context    // context which is used for all backend calls
    36  	cancelFunc context.CancelFunc // function to cancel the above context
    37  	wg         sync.WaitGroup
    38  
    39  	logger  log.Logger
    40  	backend Backend
    41  	sender  common.Address // sender of transactions which this instance can monitor
    42  
    43  	pollingInterval   time.Duration // time between checking for new blocks
    44  	cancellationDepth uint64        // number of blocks until considering a tx cancellation final
    45  
    46  	watchesByNonce map[uint64]map[common.Hash][]transactionWatch // active watches grouped by nonce and tx hash
    47  	watchAdded     chan struct{}                                 // channel to trigger instant pending check
    48  }
    49  
    50  type transactionWatch struct {
    51  	receiptC chan types.Receipt // channel to which the receipt will be written once available
    52  	errC     chan error         // error channel (primarily for cancelled transactions)
    53  }
    54  
    55  func NewMonitor(logger log.Logger, backend Backend, sender common.Address, pollingInterval time.Duration, cancellationDepth uint64) Monitor {
    56  	ctx, cancelFunc := context.WithCancel(context.Background())
    57  
    58  	t := &transactionMonitor{
    59  		ctx:        ctx,
    60  		cancelFunc: cancelFunc,
    61  		logger:     logger.WithName(loggerName).Register(),
    62  		backend:    backend,
    63  		sender:     sender,
    64  
    65  		pollingInterval:   pollingInterval,
    66  		cancellationDepth: cancellationDepth,
    67  
    68  		watchesByNonce: make(map[uint64]map[common.Hash][]transactionWatch),
    69  		watchAdded:     make(chan struct{}, 1),
    70  	}
    71  
    72  	t.wg.Add(1)
    73  	go t.watchPending()
    74  
    75  	return t
    76  }
    77  
    78  func (tm *transactionMonitor) WatchTransaction(txHash common.Hash, nonce uint64) (<-chan types.Receipt, <-chan error, error) {
    79  	loggerV1 := tm.logger.V(1).Register()
    80  
    81  	tm.lock.Lock()
    82  	defer tm.lock.Unlock()
    83  
    84  	// these channels will be written to at most once
    85  	// buffer size is 1 to avoid blocking in the watch loop
    86  	receiptC := make(chan types.Receipt, 1)
    87  	errC := make(chan error, 1)
    88  
    89  	if _, ok := tm.watchesByNonce[nonce]; !ok {
    90  		tm.watchesByNonce[nonce] = make(map[common.Hash][]transactionWatch)
    91  	}
    92  
    93  	tm.watchesByNonce[nonce][txHash] = append(tm.watchesByNonce[nonce][txHash], transactionWatch{
    94  		receiptC: receiptC,
    95  		errC:     errC,
    96  	})
    97  
    98  	select {
    99  	case tm.watchAdded <- struct{}{}:
   100  	default:
   101  	}
   102  
   103  	loggerV1.Debug("starting to watch transaction", "tx", txHash, "nonce", nonce)
   104  
   105  	return receiptC, errC, nil
   106  }
   107  
   108  // main watch loop
   109  func (tm *transactionMonitor) watchPending() {
   110  	loggerV1 := tm.logger.V(1).Register()
   111  
   112  	defer tm.wg.Done()
   113  	defer func() {
   114  		tm.lock.Lock()
   115  		defer tm.lock.Unlock()
   116  
   117  		for _, watches := range tm.watchesByNonce {
   118  			for _, txMap := range watches {
   119  				for _, watch := range txMap {
   120  					select {
   121  					case watch.errC <- ErrMonitorClosed:
   122  					default:
   123  					}
   124  				}
   125  			}
   126  		}
   127  	}()
   128  
   129  	var (
   130  		lastBlock uint64 = 0
   131  		added     bool   // flag if this iteration was triggered by the watchAdded channel
   132  	)
   133  
   134  	for {
   135  		added = false
   136  		select {
   137  		// if a new watch has been added check again without waiting
   138  		case <-tm.watchAdded:
   139  			added = true
   140  		// otherwise wait
   141  		case <-time.After(tm.pollingInterval):
   142  		// if the main context is cancelled terminate
   143  		case <-tm.ctx.Done():
   144  			return
   145  		}
   146  
   147  		// if there are no watched transactions there is nothing to do
   148  		if !tm.hasWatches() {
   149  			continue
   150  		}
   151  
   152  		// switch to new head subscriptions once websockets are the norm
   153  		block, err := tm.backend.BlockNumber(tm.ctx)
   154  		if err != nil {
   155  			tm.logger.Error(err, "could not get block number")
   156  			continue
   157  		} else if block <= lastBlock && !added {
   158  			// if the block number is not higher than before there is nothing todo
   159  			// unless a watch was added in which case we will do the check anyway
   160  			// in the rare case where a block was reorged and the new one is the first to contain our tx we wait an extra block
   161  			continue
   162  		}
   163  
   164  		if err := tm.checkPending(block); err != nil {
   165  			loggerV1.Debug("error while checking pending transactions", "error", err)
   166  			continue
   167  		}
   168  		lastBlock = block
   169  	}
   170  }
   171  
   172  // potentiallyConfirmedTxWatches returns all watches with nonce less than what was specified
   173  func (tm *transactionMonitor) potentiallyConfirmedTxWatches(nonce uint64) (watches map[uint64]map[common.Hash][]transactionWatch) {
   174  	tm.lock.Lock()
   175  	defer tm.lock.Unlock()
   176  
   177  	potentiallyConfirmedTxWatches := make(map[uint64]map[common.Hash][]transactionWatch)
   178  	for n, watches := range tm.watchesByNonce {
   179  		if n < nonce {
   180  			potentiallyConfirmedTxWatches[n] = watches
   181  		}
   182  	}
   183  
   184  	return potentiallyConfirmedTxWatches
   185  }
   186  
   187  func (tm *transactionMonitor) hasWatches() bool {
   188  	tm.lock.Lock()
   189  	defer tm.lock.Unlock()
   190  	return len(tm.watchesByNonce) > 0
   191  }
   192  
   193  // check pending checks the given block (number) for confirmed or cancelled transactions
   194  func (tm *transactionMonitor) checkPending(block uint64) error {
   195  	nonce, err := tm.backend.NonceAt(tm.ctx, tm.sender, new(big.Int).SetUint64(block))
   196  	if err != nil {
   197  		return err
   198  	}
   199  
   200  	// transactions with a nonce lower or equal to what is found on-chain are either confirmed or (at least temporarily) cancelled
   201  	potentiallyConfirmedTxWatches := tm.potentiallyConfirmedTxWatches(nonce)
   202  
   203  	confirmedNonces := make(map[uint64]*types.Receipt)
   204  	var cancelledNonces []uint64
   205  	for nonceGroup, watchMap := range potentiallyConfirmedTxWatches {
   206  		for txHash := range watchMap {
   207  			receipt, err := tm.backend.TransactionReceipt(tm.ctx, txHash)
   208  			if err != nil {
   209  				if errors.Is(err, ethereum.NotFound) {
   210  					// if both err and receipt are nil, there is no receipt
   211  					// the reason why we consider this only potentially cancelled is to catch cases where after a reorg the original transaction wins
   212  					continue
   213  				}
   214  				return err
   215  			}
   216  			if receipt != nil {
   217  				// if we have a receipt we have a confirmation
   218  				confirmedNonces[nonceGroup] = receipt
   219  			}
   220  		}
   221  	}
   222  
   223  	for nonceGroup := range potentiallyConfirmedTxWatches {
   224  		if _, ok := confirmedNonces[nonceGroup]; ok {
   225  			continue
   226  		}
   227  
   228  		oldNonce, err := tm.backend.NonceAt(tm.ctx, tm.sender, new(big.Int).SetUint64(block-tm.cancellationDepth))
   229  		if err != nil {
   230  			return err
   231  		}
   232  
   233  		if nonceGroup < oldNonce {
   234  			cancelledNonces = append(cancelledNonces, nonceGroup)
   235  		}
   236  	}
   237  
   238  	// notify the subscribers and remove watches for confirmed or cancelled transactions
   239  	tm.lock.Lock()
   240  	defer tm.lock.Unlock()
   241  
   242  	for nonce, receipt := range confirmedNonces {
   243  		for txHash, watches := range potentiallyConfirmedTxWatches[nonce] {
   244  			if receipt.TxHash == txHash {
   245  				for _, watch := range watches {
   246  					select {
   247  					case watch.receiptC <- *receipt:
   248  					default:
   249  					}
   250  				}
   251  			} else {
   252  				for _, watch := range watches {
   253  					select {
   254  					case watch.errC <- ErrTransactionCancelled:
   255  					default:
   256  					}
   257  				}
   258  			}
   259  		}
   260  		delete(tm.watchesByNonce, nonce)
   261  	}
   262  
   263  	for _, nonce := range cancelledNonces {
   264  		for _, watches := range tm.watchesByNonce[nonce] {
   265  			for _, watch := range watches {
   266  				select {
   267  				case watch.errC <- ErrTransactionCancelled:
   268  				default:
   269  				}
   270  			}
   271  		}
   272  		delete(tm.watchesByNonce, nonce)
   273  	}
   274  
   275  	return nil
   276  }
   277  
   278  func (tm *transactionMonitor) Close() error {
   279  	tm.cancelFunc()
   280  	tm.wg.Wait()
   281  	return nil
   282  }