github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/core/ledger/kvledger/txmgmt/pvtstatepurgemgmt/purge_mgr.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package pvtstatepurgemgmt 8 9 import ( 10 "math" 11 "sync" 12 13 "github.com/osdi23p228/fabric/core/ledger/internal/version" 14 "github.com/osdi23p228/fabric/core/ledger/kvledger/bookkeeping" 15 "github.com/osdi23p228/fabric/core/ledger/kvledger/txmgmt/privacyenabledstate" 16 "github.com/osdi23p228/fabric/core/ledger/kvledger/txmgmt/statedb" 17 "github.com/osdi23p228/fabric/core/ledger/pvtdatapolicy" 18 "github.com/osdi23p228/fabric/core/ledger/util" 19 ) 20 21 type PurgeMgr struct { 22 btlPolicy pvtdatapolicy.BTLPolicy 23 db *privacyenabledstate.DB 24 expKeeper *expiryKeeper 25 26 lock *sync.Mutex 27 waitGrp *sync.WaitGroup 28 29 workingset *workingset 30 } 31 32 type workingset struct { 33 toPurge expiryInfoMap 34 toClearFromSchedule []*expiryInfoKey 35 expiringBlk uint64 36 err error 37 } 38 39 type expiryInfoMap map[privacyenabledstate.HashedCompositeKey]*keyAndVersion 40 41 type keyAndVersion struct { 42 key string 43 committingBlock uint64 44 purgeKeyOnly bool 45 } 46 47 // InstantiatePurgeMgr instantiates a PurgeMgr. 48 func InstantiatePurgeMgr(ledgerid string, db *privacyenabledstate.DB, btlPolicy pvtdatapolicy.BTLPolicy, bookkeepingProvider bookkeeping.Provider) (*PurgeMgr, error) { 49 return &PurgeMgr{ 50 btlPolicy: btlPolicy, 51 db: db, 52 expKeeper: newExpiryKeeper(ledgerid, bookkeepingProvider), 53 lock: &sync.Mutex{}, 54 waitGrp: &sync.WaitGroup{}, 55 }, nil 56 } 57 58 // PrepareForExpiringKeys gives a chance to the PurgeMgr to do background work in advance if any 59 func (p *PurgeMgr) PrepareForExpiringKeys(expiringAtBlk uint64) { 60 p.waitGrp.Add(1) 61 go func() { 62 p.lock.Lock() 63 p.waitGrp.Done() 64 defer p.lock.Unlock() 65 p.workingset = p.prepareWorkingsetFor(expiringAtBlk) 66 }() 67 p.waitGrp.Wait() 68 } 69 70 // WaitForPrepareToFinish holds the caller till the background goroutine launched by 'PrepareForExpiringKeys' is finished 71 func (p *PurgeMgr) WaitForPrepareToFinish() { 72 p.lock.Lock() 73 p.lock.Unlock() 74 } 75 76 // UpdateExpirtyInfoPvtDataOfOldBlocks updates the existing expiry entries in the expiryKeeper with the given pvtUpdates 77 func (p *PurgeMgr) UpdateExpiryInfoOfPvtDataOfOldBlocks(pvtUpdates *privacyenabledstate.PvtUpdateBatch) error { 78 builder := newExpiryScheduleBuilder(p.btlPolicy) 79 pvtUpdateCompositeKeyMap := pvtUpdates.ToCompositeKeyMap() 80 for k, vv := range pvtUpdateCompositeKeyMap { 81 if err := builder.add(k.Namespace, k.CollectionName, k.Key, util.ComputeStringHash(k.Key), vv); err != nil { 82 return err 83 } 84 } 85 86 var expiryInfoUpdates []*expiryInfo 87 for _, toAdd := range builder.getExpiryInfo() { 88 toUpdate, err := p.expKeeper.retrieveByExpiryKey(toAdd.expiryInfoKey) 89 if err != nil { 90 return err 91 } 92 // Though we could update the existing entry (as there should be one due 93 // to only the keyHash of this pvtUpdateKey), for simplicity and to be less 94 // expensive, we append a new entry 95 toUpdate.pvtdataKeys.addAll(toAdd.pvtdataKeys) 96 expiryInfoUpdates = append(expiryInfoUpdates, toUpdate) 97 } 98 99 // As the expiring keys list might have been constructed after the last 100 // regular block commit, we need to update the list. This is because, 101 // some of the old pvtData which are being committed might get expired 102 // during the next regular block commit. As a result, the corresponding 103 // hashedKey in the expiring keys list would be missing the pvtData. 104 p.addMissingPvtDataToWorkingSet(pvtUpdateCompositeKeyMap) 105 106 return p.expKeeper.update(expiryInfoUpdates, nil) 107 } 108 109 func (p *PurgeMgr) addMissingPvtDataToWorkingSet(pvtKeys privacyenabledstate.PvtdataCompositeKeyMap) { 110 if p.workingset == nil || len(p.workingset.toPurge) == 0 { 111 return 112 } 113 114 for k := range pvtKeys { 115 hashedCompositeKey := privacyenabledstate.HashedCompositeKey{ 116 Namespace: k.Namespace, 117 CollectionName: k.CollectionName, 118 KeyHash: string(util.ComputeStringHash(k.Key))} 119 120 toPurgeKey, ok := p.workingset.toPurge[hashedCompositeKey] 121 if !ok { 122 // corresponding hashedKey is not present in the 123 // expiring keys list 124 continue 125 } 126 127 // if the purgeKeyOnly is set, it means that the version of the pvtKey 128 // stored in the stateDB is older than the version of the hashedKey. 129 // As a result, only the pvtKey needs to be purged (expiring block height 130 // for the recent hashedKey would be higher). If the recent 131 // pvtKey of the corresponding hashedKey is being committed, we need to 132 // remove the purgeKeyOnly entries from the toPurgeList it is going to be 133 // updated by the commit of missing pvtData 134 if toPurgeKey.purgeKeyOnly { 135 delete(p.workingset.toPurge, hashedCompositeKey) 136 } else { 137 toPurgeKey.key = k.Key 138 } 139 } 140 } 141 142 func (p *PurgeMgr) UpdateExpiryInfo( 143 pvtUpdates *privacyenabledstate.PvtUpdateBatch, 144 hashedUpdates *privacyenabledstate.HashedUpdateBatch) error { 145 expiryInfoUpdates, err := buildExpirySchedule(p.btlPolicy, pvtUpdates, hashedUpdates) 146 if err != nil { 147 return err 148 } 149 return p.expKeeper.update(expiryInfoUpdates, nil) 150 } 151 152 // AddExpiredEntriesToUpdateBatch add the expired pvtdata to the updateBatch of next block to be committed 153 func (p *PurgeMgr) AddExpiredEntriesToUpdateBatch( 154 pvtUpdates *privacyenabledstate.PvtUpdateBatch, 155 hashedUpdates *privacyenabledstate.HashedUpdateBatch) error { 156 p.lock.Lock() 157 defer p.lock.Unlock() 158 if p.workingset.err != nil { 159 return p.workingset.err 160 } 161 162 // For each key selected for purging, check if the key is not getting updated in the current block, 163 // add its deletion in the update batches for pvt and hashed updates 164 for compositeHashedKey, keyAndVersion := range p.workingset.toPurge { 165 ns := compositeHashedKey.Namespace 166 coll := compositeHashedKey.CollectionName 167 keyHash := []byte(compositeHashedKey.KeyHash) 168 key := keyAndVersion.key 169 purgeKeyOnly := keyAndVersion.purgeKeyOnly 170 hashUpdated := hashedUpdates.Contains(ns, coll, keyHash) 171 pvtKeyUpdated := pvtUpdates.Contains(ns, coll, key) 172 173 logger.Debugf("Checking whether the key [ns=%s, coll=%s, keyHash=%x, purgeKeyOnly=%t] "+ 174 "is updated in the update batch for the committing block - hashUpdated=%t, and pvtKeyUpdated=%t", 175 ns, coll, keyHash, purgeKeyOnly, hashUpdated, pvtKeyUpdated) 176 177 expiringTxVersion := version.NewHeight(p.workingset.expiringBlk, math.MaxUint64) 178 if !hashUpdated && !purgeKeyOnly { 179 logger.Debugf("Adding the hashed key to be purged to the delete list in the update batch") 180 hashedUpdates.Delete(ns, coll, keyHash, expiringTxVersion) 181 } 182 if key != "" && !pvtKeyUpdated { 183 logger.Debugf("Adding the pvt key to be purged to the delete list in the update batch") 184 pvtUpdates.Delete(ns, coll, key, expiringTxVersion) 185 } 186 } 187 return nil 188 } 189 190 // BlockCommitDone is a callback to the PurgeMgr when the block is committed to the ledger 191 // These orphan entries for purge-schedule can be cleared off in bulk in a separate background routine as well 192 // If we maintain the following logic (i.e., clear off entries just after block commit), we need a TODO - 193 // We need to perform a check in the start, because there could be a crash between the block commit and 194 // invocation to this function resulting in the orphan entry for the deletes scheduled for the last block 195 // Also, the another way is to club the delete of these entries in the same batch that adds entries for the future expirations - 196 // however, that requires updating the expiry store by replaying the last block from blockchain in order to sustain a crash between 197 // entries updates and block commit 198 func (p *PurgeMgr) BlockCommitDone() error { 199 defer func() { p.workingset = nil }() 200 return p.expKeeper.update(nil, p.workingset.toClearFromSchedule) 201 } 202 203 // prepareWorkingsetFor returns a working set for a given expiring block 'expiringAtBlk'. 204 // This working set contains the pvt data keys that will expire with the commit of block 'expiringAtBlk'. 205 func (p *PurgeMgr) prepareWorkingsetFor(expiringAtBlk uint64) *workingset { 206 logger.Debugf("Preparing potential purge list working-set for expiringAtBlk [%d]", expiringAtBlk) 207 workingset := &workingset{expiringBlk: expiringAtBlk} 208 // Retrieve the keys from expiryKeeper 209 expiryInfo, err := p.expKeeper.retrieve(expiringAtBlk) 210 if err != nil { 211 workingset.err = err 212 return workingset 213 } 214 // Transform the keys into the form such that for each hashed key that is eligible for purge appears in 'toPurge' 215 toPurge := transformToExpiryInfoMap(expiryInfo) 216 // Load the latest versions of the hashed keys 217 if err = p.preloadCommittedVersionsInCache(toPurge); err != nil { 218 workingset.err = err 219 return workingset 220 } 221 var expiryInfoKeysToClear []*expiryInfoKey 222 223 if len(toPurge) == 0 { 224 logger.Debugf("No expiry entry found for expiringAtBlk [%d]", expiringAtBlk) 225 return workingset 226 } 227 logger.Debugf("Total [%d] expiring entries found. Evaluating whether some of these keys have been overwritten in later blocks...", len(toPurge)) 228 229 for purgeEntryK, purgeEntryV := range toPurge { 230 logger.Debugf("Evaluating for hashedKey [%s]", purgeEntryK) 231 expiryInfoKeysToClear = append(expiryInfoKeysToClear, &expiryInfoKey{committingBlk: purgeEntryV.committingBlock, expiryBlk: expiringAtBlk}) 232 currentVersion, err := p.db.GetKeyHashVersion(purgeEntryK.Namespace, purgeEntryK.CollectionName, []byte(purgeEntryK.KeyHash)) 233 if err != nil { 234 workingset.err = err 235 return workingset 236 } 237 238 if sameVersion(currentVersion, purgeEntryV.committingBlock) { 239 logger.Debugf( 240 "The version of the hashed key in the committed state and in the expiry entry is same " + 241 "hence, keeping the entry in the purge list") 242 continue 243 } 244 245 logger.Debugf("The version of the hashed key in the committed state and in the expiry entry is different") 246 if purgeEntryV.key != "" { 247 logger.Debugf("The expiry entry also contains the raw key along with the key hash") 248 committedPvtVerVal, err := p.db.GetPrivateData(purgeEntryK.Namespace, purgeEntryK.CollectionName, purgeEntryV.key) 249 if err != nil { 250 workingset.err = err 251 return workingset 252 } 253 254 if sameVersionFromVal(committedPvtVerVal, purgeEntryV.committingBlock) { 255 logger.Debugf( 256 "The version of the pvt key in the committed state and in the expiry entry is same" + 257 "Including only key in the purge list and not the hashed key") 258 purgeEntryV.purgeKeyOnly = true 259 continue 260 } 261 } 262 263 // If we reached here, the keyhash and private key (if present, in the expiry entry) have been updated in a later block, therefore remove from current purge list 264 logger.Debugf("Removing from purge list - the key hash and key (if present, in the expiry entry)") 265 delete(toPurge, purgeEntryK) 266 } 267 // Final keys to purge from state 268 workingset.toPurge = toPurge 269 // Keys to clear from expiryKeeper 270 workingset.toClearFromSchedule = expiryInfoKeysToClear 271 return workingset 272 } 273 274 func (p *PurgeMgr) preloadCommittedVersionsInCache(expInfoMap expiryInfoMap) error { 275 if !p.db.IsBulkOptimizable() { 276 return nil 277 } 278 var hashedKeys []*privacyenabledstate.HashedCompositeKey 279 for k := range expInfoMap { 280 hashedKeys = append(hashedKeys, &k) 281 } 282 return p.db.LoadCommittedVersionsOfPubAndHashedKeys(nil, hashedKeys) 283 } 284 285 func transformToExpiryInfoMap(expiryInfo []*expiryInfo) expiryInfoMap { 286 expinfoMap := make(expiryInfoMap) 287 for _, expinfo := range expiryInfo { 288 for ns, colls := range expinfo.pvtdataKeys.Map { 289 for coll, keysAndHashes := range colls.Map { 290 for _, keyAndHash := range keysAndHashes.List { 291 compositeKey := privacyenabledstate.HashedCompositeKey{Namespace: ns, CollectionName: coll, KeyHash: string(keyAndHash.Hash)} 292 expinfoMap[compositeKey] = &keyAndVersion{key: keyAndHash.Key, committingBlock: expinfo.expiryInfoKey.committingBlk} 293 } 294 } 295 } 296 } 297 return expinfoMap 298 } 299 300 func sameVersion(version *version.Height, blockNum uint64) bool { 301 return version != nil && version.BlockNum == blockNum 302 } 303 304 func sameVersionFromVal(vv *statedb.VersionedValue, blockNum uint64) bool { 305 return vv != nil && sameVersion(vv.Version, blockNum) 306 }