github.com/status-im/status-go@v1.1.0/services/wallet/transfer/concurrent.go (about)

     1  package transfer
     2  
     3  import (
     4  	"context"
     5  	"math/big"
     6  	"sort"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/pkg/errors"
    11  
    12  	"github.com/ethereum/go-ethereum/common"
    13  	"github.com/ethereum/go-ethereum/log"
    14  	"github.com/status-im/status-go/services/wallet/async"
    15  	"github.com/status-im/status-go/services/wallet/balance"
    16  )
    17  
    18  const (
    19  	NoThreadLimit         uint32 = 0
    20  	SequentialThreadLimit uint32 = 10
    21  )
    22  
    23  // NewConcurrentDownloader creates ConcurrentDownloader instance.
    24  func NewConcurrentDownloader(ctx context.Context, limit uint32) *ConcurrentDownloader {
    25  	runner := async.NewQueuedAtomicGroup(ctx, limit)
    26  	result := &Result{}
    27  	return &ConcurrentDownloader{runner, result}
    28  }
    29  
    30  type ConcurrentDownloader struct {
    31  	*async.QueuedAtomicGroup
    32  	*Result
    33  }
    34  
    35  type Result struct {
    36  	mu          sync.Mutex
    37  	transfers   []Transfer
    38  	headers     []*DBHeader
    39  	blockRanges [][]*big.Int
    40  }
    41  
    42  var errDownloaderStuck = errors.New("eth downloader is stuck")
    43  
    44  func (r *Result) Push(transfers ...Transfer) {
    45  	r.mu.Lock()
    46  	defer r.mu.Unlock()
    47  	r.transfers = append(r.transfers, transfers...)
    48  }
    49  
    50  func (r *Result) Get() []Transfer {
    51  	r.mu.Lock()
    52  	defer r.mu.Unlock()
    53  	rst := make([]Transfer, len(r.transfers))
    54  	copy(rst, r.transfers)
    55  	return rst
    56  }
    57  
    58  func (r *Result) PushHeader(block *DBHeader) {
    59  	r.mu.Lock()
    60  	defer r.mu.Unlock()
    61  
    62  	r.headers = append(r.headers, block)
    63  }
    64  
    65  func (r *Result) GetHeaders() []*DBHeader {
    66  	r.mu.Lock()
    67  	defer r.mu.Unlock()
    68  	rst := make([]*DBHeader, len(r.headers))
    69  	copy(rst, r.headers)
    70  	return rst
    71  }
    72  
    73  func (r *Result) PushRange(blockRange []*big.Int) {
    74  	r.mu.Lock()
    75  	defer r.mu.Unlock()
    76  
    77  	r.blockRanges = append(r.blockRanges, blockRange)
    78  }
    79  
    80  func (r *Result) GetRanges() [][]*big.Int {
    81  	r.mu.Lock()
    82  	defer r.mu.Unlock()
    83  	rst := make([][]*big.Int, len(r.blockRanges))
    84  	copy(rst, r.blockRanges)
    85  	r.blockRanges = [][]*big.Int{}
    86  
    87  	return rst
    88  }
    89  
    90  // Downloader downloads transfers from single block using number.
    91  type Downloader interface {
    92  	GetTransfersByNumber(context.Context, *big.Int) ([]Transfer, error)
    93  }
    94  
    95  // Returns new block ranges that contain transfers and found block headers that contain transfers, and a block where
    96  // beginning of trasfers history detected
    97  func checkRangesWithStartBlock(parent context.Context, client balance.Reader, cache balance.Cacher,
    98  	account common.Address, ranges [][]*big.Int, threadLimit uint32, startBlock *big.Int) (
    99  	resRanges [][]*big.Int, headers []*DBHeader, newStartBlock *big.Int, err error) {
   100  
   101  	log.Debug("start checkRanges", "account", account.Hex(), "ranges len", len(ranges), "startBlock", startBlock)
   102  
   103  	ctx, cancel := context.WithTimeout(parent, 30*time.Second)
   104  	defer cancel()
   105  
   106  	c := NewConcurrentDownloader(ctx, threadLimit)
   107  
   108  	newStartBlock = startBlock
   109  
   110  	for _, blocksRange := range ranges {
   111  		from := blocksRange[0]
   112  		to := blocksRange[1]
   113  
   114  		log.Debug("check block range", "from", from, "to", to)
   115  
   116  		if startBlock != nil {
   117  			if to.Cmp(newStartBlock) <= 0 {
   118  				log.Debug("'to' block is less than 'start' block", "to", to, "startBlock", startBlock)
   119  				continue
   120  			}
   121  		}
   122  
   123  		c.Add(func(ctx context.Context) error {
   124  			if from.Cmp(to) >= 0 {
   125  				log.Debug("'from' block is greater than or equal to 'to' block", "from", from, "to", to)
   126  				return nil
   127  			}
   128  			log.Debug("eth transfers comparing blocks", "from", from, "to", to)
   129  
   130  			if startBlock != nil {
   131  				if to.Cmp(startBlock) <= 0 {
   132  					log.Debug("'to' block is less than 'start' block", "to", to, "startBlock", startBlock)
   133  					return nil
   134  				}
   135  			}
   136  
   137  			lb, err := cache.BalanceAt(ctx, client, account, from)
   138  			if err != nil {
   139  				return err
   140  			}
   141  			hb, err := cache.BalanceAt(ctx, client, account, to)
   142  			if err != nil {
   143  				return err
   144  			}
   145  			if lb.Cmp(hb) == 0 {
   146  				log.Debug("balances are equal", "from", from, "to", to, "lb", lb, "hb", hb)
   147  
   148  				hn, err := cache.NonceAt(ctx, client, account, to)
   149  				if err != nil {
   150  					return err
   151  				}
   152  				// if nonce is zero in a newer block then there is no need to check an older one
   153  				if *hn == 0 {
   154  					log.Debug("zero nonce", "to", to)
   155  
   156  					if hb.Cmp(big.NewInt(0)) == 0 { // balance is 0, nonce is 0, we stop checking further, that will be the start block (even though the real one can be a later one)
   157  						if startBlock != nil {
   158  							if to.Cmp(newStartBlock) > 0 {
   159  								log.Debug("found possible start block, we should not search back", "block", to)
   160  								newStartBlock = to // increase newStartBlock if we found a new higher block
   161  							}
   162  						} else {
   163  							newStartBlock = to
   164  						}
   165  					}
   166  
   167  					return nil
   168  				}
   169  
   170  				ln, err := cache.NonceAt(ctx, client, account, from)
   171  				if err != nil {
   172  					return err
   173  				}
   174  				if *ln == *hn {
   175  					log.Debug("transaction count is also equal", "from", from, "to", to, "ln", *ln, "hn", *hn)
   176  					return nil
   177  				}
   178  			}
   179  			if new(big.Int).Sub(to, from).Cmp(one) == 0 {
   180  				// WARNING: Block hash calculation from plain header returns a wrong value.
   181  				header, err := client.HeaderByNumber(ctx, to)
   182  				if err != nil {
   183  					return err
   184  				}
   185  				// Obtain block hash from first transaction
   186  				blockHash, err := client.CallBlockHashByTransaction(ctx, to, 0)
   187  				if err != nil {
   188  					return err
   189  				}
   190  				c.PushHeader(toDBHeader(header, blockHash, account))
   191  				return nil
   192  			}
   193  			mid := new(big.Int).Add(from, to)
   194  			mid = mid.Div(mid, two)
   195  			_, err = cache.BalanceAt(ctx, client, account, mid)
   196  			if err != nil {
   197  				return err
   198  			}
   199  			log.Debug("balances are not equal", "from", from, "mid", mid, "to", to)
   200  
   201  			c.PushRange([]*big.Int{mid, to})
   202  			c.PushRange([]*big.Int{from, mid})
   203  			return nil
   204  		})
   205  	}
   206  
   207  	select {
   208  	case <-c.WaitAsync():
   209  	case <-ctx.Done():
   210  		return nil, nil, nil, errDownloaderStuck
   211  	}
   212  
   213  	if c.Error() != nil {
   214  		return nil, nil, nil, errors.Wrap(c.Error(), "failed to dowload transfers using concurrent downloader")
   215  	}
   216  
   217  	log.Debug("end checkRanges", "account", account.Hex(), "newStartBlock", newStartBlock)
   218  	return c.GetRanges(), c.GetHeaders(), newStartBlock, nil
   219  }
   220  
   221  func findBlocksWithEthTransfers(parent context.Context, client balance.Reader, cache balance.Cacher,
   222  	account common.Address, low, high *big.Int, noLimit bool, threadLimit uint32) (
   223  	from *big.Int, headers []*DBHeader, resStartBlock *big.Int, err error) {
   224  
   225  	ranges := [][]*big.Int{{low, high}}
   226  	from = big.NewInt(low.Int64())
   227  	headers = []*DBHeader{}
   228  	var lvl = 1
   229  
   230  	for len(ranges) > 0 && lvl <= 30 {
   231  		log.Debug("check blocks ranges", "lvl", lvl, "ranges len", len(ranges))
   232  		lvl++
   233  		// Check if there are transfers in blocks in ranges. To do that, nonce and balance is checked
   234  		// the block ranges that have transfers are returned
   235  		newRanges, newHeaders, strtBlock, err := checkRangesWithStartBlock(parent, client, cache,
   236  			account, ranges, threadLimit, resStartBlock)
   237  		resStartBlock = strtBlock
   238  		if err != nil {
   239  			return nil, nil, nil, err
   240  		}
   241  
   242  		headers = append(headers, newHeaders...)
   243  
   244  		if len(newRanges) > 0 {
   245  			log.Debug("found new ranges", "account", account, "lvl", lvl, "new ranges len", len(newRanges))
   246  		}
   247  		if len(newRanges) > 60 && !noLimit {
   248  			sort.SliceStable(newRanges, func(i, j int) bool {
   249  				return newRanges[i][0].Cmp(newRanges[j][0]) == 1
   250  			})
   251  
   252  			newRanges = newRanges[:60]
   253  			from = newRanges[len(newRanges)-1][0]
   254  		}
   255  
   256  		ranges = newRanges
   257  	}
   258  
   259  	return
   260  }