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 }