github.com/status-im/status-go@v1.1.0/services/rpcfilters/logs_cache.go (about)

     1  package rpcfilters
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"sync"
     7  
     8  	"github.com/ethereum/go-ethereum/common"
     9  	"github.com/ethereum/go-ethereum/core/types"
    10  )
    11  
    12  const (
    13  	defaultCacheSize = 20
    14  )
    15  
    16  type cacheRecord struct {
    17  	block uint64
    18  	hash  common.Hash
    19  	logs  []types.Log
    20  }
    21  
    22  func newCache(size int) *cache {
    23  	return &cache{
    24  		records: make([]cacheRecord, 0, size),
    25  		size:    size,
    26  	}
    27  }
    28  
    29  type cache struct {
    30  	mu      sync.RWMutex
    31  	size    int // length of the records
    32  	records []cacheRecord
    33  }
    34  
    35  // add inserts logs into cache and returns added and replaced logs.
    36  // replaced logs with will be returned with Removed=true.
    37  func (c *cache) add(logs []types.Log) (added, replaced []types.Log, err error) {
    38  	if len(logs) == 0 {
    39  		return nil, nil, nil
    40  	}
    41  	aggregated := aggregateLogs(logs, c.size) // size doesn't change
    42  	if len(aggregated) == 0 {
    43  		return nil, nil, nil
    44  	}
    45  	if err := checkLogsAreInOrder(aggregated); err != nil {
    46  		return nil, nil, err
    47  	}
    48  	c.mu.Lock()
    49  	defer c.mu.Unlock()
    50  	// find common block. e.g. [3,4] and [1,2,3,4] = 3
    51  	last := 0
    52  	if len(c.records) > 0 {
    53  		last = len(c.records) - 1
    54  		for aggregated[0].block < c.records[last].block && last > 0 {
    55  			last--
    56  		}
    57  	}
    58  	c.records, added, replaced = merge(last, c.records, aggregated)
    59  	if lth := len(c.records); lth > c.size {
    60  		copy(c.records, c.records[lth-c.size:])
    61  	}
    62  	return added, replaced, nil
    63  }
    64  
    65  func (c *cache) earliestBlockNum() uint64 {
    66  	if len(c.records) == 0 {
    67  		return 0
    68  	}
    69  	return c.records[0].block
    70  }
    71  
    72  func checkLogsAreInOrder(records []cacheRecord) error {
    73  	for prev, i := 0, 1; i < len(records); i++ {
    74  		if records[prev].block == records[i].block-1 {
    75  			prev = i
    76  		} else {
    77  			return fmt.Errorf(
    78  				"logs must be delivered straight in order. gaps between blocks '%d' and '%d'",
    79  				records[prev].block, records[i].block,
    80  			)
    81  		}
    82  	}
    83  	return nil
    84  }
    85  
    86  // merge merges received records into old slice starting at provided position, example:
    87  // [1, 2, 3]
    88  //
    89  //	[2, 3, 4]
    90  //
    91  // [1, 2, 3, 4]
    92  // if hash doesn't match previously received hash - such block was removed due to reorg
    93  // logs that were a part of that block will be returned with Removed set to true
    94  func merge(last int, old, received []cacheRecord) ([]cacheRecord, []types.Log, []types.Log) {
    95  	var (
    96  		added, replaced []types.Log
    97  		block           uint64
    98  		hash            common.Hash
    99  	)
   100  	for i := range received {
   101  		record := received[i]
   102  		if last < len(old) {
   103  			block = old[last].block
   104  			hash = old[last].hash
   105  		}
   106  		if record.block > block {
   107  			// simply add new records
   108  			added = append(added, record.logs...)
   109  			old = append(old, record)
   110  		} else if record.hash != hash && record.block == block {
   111  			// record hash is not equal to previous record hash at the same height
   112  			// replace record in hash and add logs as replaced
   113  			replaced = append(replaced, old[last].logs...)
   114  			added = append(added, record.logs...)
   115  			old[last] = record
   116  		}
   117  		last++
   118  	}
   119  	return old, added, replaced
   120  }
   121  
   122  // aggregateLogs creates at most requested amount of cacheRecords from provided logs.
   123  // cacheRecords will be sorted in ascending order, starting from lowest block to highest.
   124  func aggregateLogs(logs []types.Log, limit int) []cacheRecord {
   125  	// sort in reverse order, so that iteration will start from latest blocks
   126  	sort.Slice(logs, func(i, j int) bool {
   127  		return logs[i].BlockNumber > logs[j].BlockNumber
   128  	})
   129  	rst := make([]cacheRecord, limit)
   130  	pos, start := len(rst)-1, 0
   131  	var hash common.Hash
   132  	for i := range logs {
   133  		log := logs[i]
   134  		if (hash != common.Hash{}) && hash != log.BlockHash {
   135  			rst[pos].logs = logs[start:i]
   136  			start = i
   137  			if pos-1 < 0 {
   138  				break
   139  			}
   140  			pos--
   141  		}
   142  		rst[pos].logs = logs[start:]
   143  		rst[pos].block = log.BlockNumber
   144  		rst[pos].hash = log.BlockHash
   145  		hash = log.BlockHash
   146  	}
   147  	return rst[pos:]
   148  }