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 }