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 }