github.com/hechain20/hechain@v0.0.0-20220316014945-b544036ba106/discovery/authcache.go (about) 1 /* 2 Copyright hechain. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package discovery 8 9 import ( 10 "encoding/asn1" 11 "encoding/hex" 12 "sync" 13 14 "github.com/hechain20/hechain/common/util" 15 "github.com/hechain20/hechain/protoutil" 16 "github.com/pkg/errors" 17 ) 18 19 const ( 20 defaultMaxCacheSize = 1000 21 defaultRetentionRatio = 0.75 22 ) 23 24 // asBytes is the function that is used to marshal protoutil.SignedData to bytes 25 var asBytes = asn1.Marshal 26 27 type acSupport interface { 28 // Eligible returns whether the given peer is eligible for receiving 29 // service from the discovery service for a given channel 30 EligibleForService(channel string, data protoutil.SignedData) error 31 32 // ConfigSequence returns the configuration sequence of the given channel 33 ConfigSequence(channel string) uint64 34 } 35 36 type authCacheConfig struct { 37 enabled bool 38 // maxCacheSize is the maximum size of the cache, after which 39 // a purge takes place 40 maxCacheSize int 41 // purgeRetentionRatio is the % of entries that remain in the cache 42 // after the cache is purged due to overpopulation 43 purgeRetentionRatio float64 44 } 45 46 // authCache defines an interface that authenticates a request in a channel context, 47 // and does memoization of invocations 48 type authCache struct { 49 credentialCache map[string]*accessCache 50 acSupport 51 sync.RWMutex 52 conf authCacheConfig 53 } 54 55 func newAuthCache(s acSupport, conf authCacheConfig) *authCache { 56 return &authCache{ 57 acSupport: s, 58 credentialCache: make(map[string]*accessCache), 59 conf: conf, 60 } 61 } 62 63 // Eligible returns whether the given peer is eligible for receiving 64 // service from the discovery service for a given channel 65 func (ac *authCache) EligibleForService(channel string, data protoutil.SignedData) error { 66 if !ac.conf.enabled { 67 return ac.acSupport.EligibleForService(channel, data) 68 } 69 // Check whether we already have a cache for this channel 70 ac.RLock() 71 cache := ac.credentialCache[channel] 72 ac.RUnlock() 73 if cache == nil { 74 // Cache for given channel wasn't found, so create a new one 75 ac.Lock() 76 cache = ac.newAccessCache(channel) 77 // And store the cache instance. 78 ac.credentialCache[channel] = cache 79 ac.Unlock() 80 } 81 return cache.EligibleForService(data) 82 } 83 84 type accessCache struct { 85 sync.RWMutex 86 channel string 87 ac *authCache 88 lastSequence uint64 89 entries map[string]error 90 } 91 92 func (ac *authCache) newAccessCache(channel string) *accessCache { 93 return &accessCache{ 94 channel: channel, 95 ac: ac, 96 entries: make(map[string]error), 97 } 98 } 99 100 func (cache *accessCache) EligibleForService(data protoutil.SignedData) error { 101 key, err := signedDataToKey(data) 102 if err != nil { 103 logger.Warningf("Failed computing key of signed data: +%v", err) 104 return errors.Wrap(err, "failed computing key of signed data") 105 } 106 currSeq := cache.ac.acSupport.ConfigSequence(cache.channel) 107 if cache.isValid(currSeq) { 108 foundInCache, isEligibleErr := cache.lookup(key) 109 if foundInCache { 110 return isEligibleErr 111 } 112 } else { 113 cache.configChange(currSeq) 114 } 115 116 // Make sure the cache doesn't overpopulate. 117 // It might happen that it overgrows the maximum size due to concurrent 118 // goroutines waiting on the lock above, but that's acceptable. 119 cache.purgeEntriesIfNeeded() 120 121 // Compute the eligibility of the client for the service 122 err = cache.ac.acSupport.EligibleForService(cache.channel, data) 123 cache.Lock() 124 defer cache.Unlock() 125 // Check if the sequence hasn't changed since last time 126 if currSeq != cache.ac.acSupport.ConfigSequence(cache.channel) { 127 // The sequence at which we computed the eligibility might have changed, 128 // so we can't put it into the cache because a more fresh computation result 129 // might already be present in the cache by now, and we don't want to override it 130 // with a stale computation result, so just return the result. 131 return err 132 } 133 // Else, the eligibility of the client has been computed under the latest sequence, 134 // so store the computation result in the cache 135 cache.entries[key] = err 136 return err 137 } 138 139 func (cache *accessCache) isPurgeNeeded() bool { 140 cache.RLock() 141 defer cache.RUnlock() 142 return len(cache.entries)+1 > cache.ac.conf.maxCacheSize 143 } 144 145 func (cache *accessCache) purgeEntriesIfNeeded() { 146 if !cache.isPurgeNeeded() { 147 return 148 } 149 150 cache.Lock() 151 defer cache.Unlock() 152 153 maxCacheSize := cache.ac.conf.maxCacheSize 154 purgeRatio := cache.ac.conf.purgeRetentionRatio 155 entries2evict := maxCacheSize - int(purgeRatio*float64(maxCacheSize)) 156 157 for key := range cache.entries { 158 if entries2evict == 0 { 159 return 160 } 161 entries2evict-- 162 delete(cache.entries, key) 163 } 164 } 165 166 func (cache *accessCache) isValid(currSeq uint64) bool { 167 cache.RLock() 168 defer cache.RUnlock() 169 return currSeq == cache.lastSequence 170 } 171 172 func (cache *accessCache) configChange(currSeq uint64) { 173 cache.Lock() 174 defer cache.Unlock() 175 cache.lastSequence = currSeq 176 // Invalidate entries 177 cache.entries = make(map[string]error) 178 } 179 180 func (cache *accessCache) lookup(key string) (cacheHit bool, lookupResult error) { 181 cache.RLock() 182 defer cache.RUnlock() 183 184 lookupResult, cacheHit = cache.entries[key] 185 return 186 } 187 188 func signedDataToKey(data protoutil.SignedData) (string, error) { 189 b, err := asBytes(data) 190 if err != nil { 191 return "", errors.Wrap(err, "failed marshaling signed data") 192 } 193 return hex.EncodeToString(util.ComputeSHA256(b)), nil 194 }