github.com/deso-protocol/core@v1.2.9/lib/block_view_like.go (about) 1 package lib 2 3 import ( 4 "fmt" 5 "github.com/btcsuite/btcd/btcec" 6 "github.com/golang/glog" 7 "github.com/pkg/errors" 8 "reflect" 9 ) 10 11 func (bav *UtxoView) _getLikeEntryForLikeKey(likeKey *LikeKey) *LikeEntry { 12 // If an entry exists in the in-memory map, return the value of that mapping. 13 mapValue, existsMapValue := bav.LikeKeyToLikeEntry[*likeKey] 14 if existsMapValue { 15 return mapValue 16 } 17 18 // If we get here it means no value exists in our in-memory map. In this case, 19 // defer to the db. If a mapping exists in the db, return it. If not, return 20 // nil. Either way, save the value to the in-memory view mapping got later. 21 likeExists := false 22 if bav.Postgres != nil { 23 likeExists = bav.Postgres.GetLike(likeKey.LikerPubKey[:], &likeKey.LikedPostHash) != nil 24 } else { 25 likeExists = DbGetLikerPubKeyToLikedPostHashMapping(bav.Handle, likeKey.LikerPubKey[:], likeKey.LikedPostHash) != nil 26 } 27 28 if likeExists { 29 likeEntry := LikeEntry{ 30 LikerPubKey: likeKey.LikerPubKey[:], 31 LikedPostHash: &likeKey.LikedPostHash, 32 } 33 bav._setLikeEntryMappings(&likeEntry) 34 return &likeEntry 35 } 36 37 return nil 38 } 39 40 func (bav *UtxoView) _setLikeEntryMappings(likeEntry *LikeEntry) { 41 // This function shouldn't be called with nil. 42 if likeEntry == nil { 43 glog.Errorf("_setLikeEntryMappings: Called with nil LikeEntry; " + 44 "this should never happen.") 45 return 46 } 47 48 likeKey := MakeLikeKey(likeEntry.LikerPubKey, *likeEntry.LikedPostHash) 49 bav.LikeKeyToLikeEntry[likeKey] = likeEntry 50 } 51 52 func (bav *UtxoView) _deleteLikeEntryMappings(likeEntry *LikeEntry) { 53 54 // Create a tombstone entry. 55 tombstoneLikeEntry := *likeEntry 56 tombstoneLikeEntry.isDeleted = true 57 58 // Set the mappings to point to the tombstone entry. 59 bav._setLikeEntryMappings(&tombstoneLikeEntry) 60 } 61 62 func (bav *UtxoView) GetLikedByReader(readerPK []byte, postHash *BlockHash) bool { 63 // Get like state. 64 likeKey := MakeLikeKey(readerPK, *postHash) 65 likeEntry := bav._getLikeEntryForLikeKey(&likeKey) 66 return likeEntry != nil && !likeEntry.isDeleted 67 } 68 69 func (bav *UtxoView) GetLikesForPostHash(postHash *BlockHash) (_likerPubKeys [][]byte, _err error) { 70 if bav.Postgres != nil { 71 likes := bav.Postgres.GetLikesForPost(postHash) 72 for _, like := range likes { 73 bav._setLikeEntryMappings(like.NewLikeEntry()) 74 } 75 } else { 76 handle := bav.Handle 77 dbPrefix := append([]byte{}, _PrefixLikedPostHashToLikerPubKey...) 78 dbPrefix = append(dbPrefix, postHash[:]...) 79 keysFound, _ := EnumerateKeysForPrefix(handle, dbPrefix) 80 81 // Iterate over all the db keys & values and load them into the view. 82 expectedKeyLength := 1 + HashSizeBytes + btcec.PubKeyBytesLenCompressed 83 for _, key := range keysFound { 84 // Sanity check that this is a reasonable key. 85 if len(key) != expectedKeyLength { 86 return nil, fmt.Errorf("UtxoView.GetLikesForPostHash: Invalid key length found: %d", len(key)) 87 } 88 89 likerPubKey := key[1+HashSizeBytes:] 90 91 likeKey := &LikeKey{ 92 LikerPubKey: MakePkMapKey(likerPubKey), 93 LikedPostHash: *postHash, 94 } 95 96 bav._getLikeEntryForLikeKey(likeKey) 97 } 98 } 99 100 // Iterate over the view and create the final list to return. 101 likerPubKeys := [][]byte{} 102 for _, likeEntry := range bav.LikeKeyToLikeEntry { 103 if !likeEntry.isDeleted && reflect.DeepEqual(likeEntry.LikedPostHash[:], postHash[:]) { 104 likerPubKeys = append(likerPubKeys, likeEntry.LikerPubKey) 105 } 106 } 107 108 return likerPubKeys, nil 109 } 110 111 func (bav *UtxoView) _connectLike( 112 txn *MsgDeSoTxn, txHash *BlockHash, blockHeight uint32, verifySignatures bool) ( 113 _totalInput uint64, _totalOutput uint64, _utxoOps []*UtxoOperation, _err error) { 114 115 // Check that the transaction has the right TxnType. 116 if txn.TxnMeta.GetTxnType() != TxnTypeLike { 117 return 0, 0, nil, fmt.Errorf("_connectLike: called with bad TxnType %s", 118 txn.TxnMeta.GetTxnType().String()) 119 } 120 txMeta := txn.TxnMeta.(*LikeMetadata) 121 122 // Connect basic txn to get the total input and the total output without 123 // considering the transaction metadata. 124 totalInput, totalOutput, utxoOpsForTxn, err := bav._connectBasicTransfer( 125 txn, txHash, blockHeight, verifySignatures) 126 if err != nil { 127 return 0, 0, nil, errors.Wrapf(err, "_connectLike: ") 128 } 129 130 if verifySignatures { 131 // _connectBasicTransfer has already checked that the transaction is 132 // signed by the top-level public key, which we take to be the sender's 133 // public key so there is no need to verify anything further. 134 } 135 136 // At this point the inputs and outputs have been processed. Now we need to handle 137 // the metadata. 138 139 // There are two main checks that need to be done before allowing a like: 140 // - Check that the post exists 141 // - Check that the person hasn't already liked the post 142 143 // Check that the post to like actually exists. 144 existingPostEntry := bav.GetPostEntryForPostHash(txMeta.LikedPostHash) 145 if existingPostEntry == nil || existingPostEntry.isDeleted { 146 return 0, 0, nil, errors.Wrapf( 147 RuleErrorCannotLikeNonexistentPost, 148 "_connectLike: Post hash: %v", txMeta.LikedPostHash) 149 } 150 151 // At this point the code diverges and considers the like / unlike flows differently 152 // since the presence of an existing like entry has a different effect in either case. 153 154 likeKey := MakeLikeKey(txn.PublicKey, *txMeta.LikedPostHash) 155 existingLikeEntry := bav._getLikeEntryForLikeKey(&likeKey) 156 // We don't need to make a copy of the post entry because all we're modifying is the like count, 157 // which isn't stored in any of our mappings. But we make a copy here just because it's a little bit 158 // more foolproof. 159 updatedPostEntry := *existingPostEntry 160 if txMeta.IsUnlike { 161 // Ensure that there *is* an existing like entry to delete. 162 if existingLikeEntry == nil || existingLikeEntry.isDeleted { 163 return 0, 0, nil, errors.Wrapf( 164 RuleErrorCannotUnlikeWithoutAnExistingLike, 165 "_connectLike: Like key: %v", &likeKey) 166 } 167 168 // Now that we know there is a like entry, we delete it and decrement the like count. 169 bav._deleteLikeEntryMappings(existingLikeEntry) 170 updatedPostEntry.LikeCount -= 1 171 } else { 172 // Ensure that there *is not* an existing like entry. 173 if existingLikeEntry != nil && !existingLikeEntry.isDeleted { 174 return 0, 0, nil, errors.Wrapf( 175 RuleErrorLikeEntryAlreadyExists, 176 "_connectLike: Like key: %v", &likeKey) 177 } 178 179 // Now that we know there is no pre-existing like entry, we can create one and 180 // increment the likes on the liked post. 181 likeEntry := &LikeEntry{ 182 LikerPubKey: txn.PublicKey, 183 LikedPostHash: txMeta.LikedPostHash, 184 } 185 bav._setLikeEntryMappings(likeEntry) 186 updatedPostEntry.LikeCount += 1 187 } 188 189 // Set the updated post entry so it has the new like count. 190 bav._setPostEntryMappings(&updatedPostEntry) 191 192 // Add an operation to the list at the end indicating we've added a follow. 193 utxoOpsForTxn = append(utxoOpsForTxn, &UtxoOperation{ 194 Type: OperationTypeLike, 195 PrevLikeEntry: existingLikeEntry, 196 PrevLikeCount: existingPostEntry.LikeCount, 197 }) 198 199 return totalInput, totalOutput, utxoOpsForTxn, nil 200 } 201 202 func (bav *UtxoView) _disconnectLike( 203 operationType OperationType, currentTxn *MsgDeSoTxn, txnHash *BlockHash, 204 utxoOpsForTxn []*UtxoOperation, blockHeight uint32) error { 205 206 // Verify that the last operation is a Like operation 207 if len(utxoOpsForTxn) == 0 { 208 return fmt.Errorf("_disconnectLike: utxoOperations are missing") 209 } 210 operationIndex := len(utxoOpsForTxn) - 1 211 if utxoOpsForTxn[operationIndex].Type != OperationTypeLike { 212 return fmt.Errorf("_disconnectLike: Trying to revert "+ 213 "OperationTypeLike but found type %v", 214 utxoOpsForTxn[operationIndex].Type) 215 } 216 217 // Now we know the txMeta is a Like 218 txMeta := currentTxn.TxnMeta.(*LikeMetadata) 219 220 // Before we do anything, let's get the post so we can adjust the like counter later. 221 likedPostEntry := bav.GetPostEntryForPostHash(txMeta.LikedPostHash) 222 if likedPostEntry == nil { 223 return fmt.Errorf("_disconnectLike: Error getting post: %v", txMeta.LikedPostHash) 224 } 225 226 // Here we diverge and consider the like and unlike cases separately. 227 if txMeta.IsUnlike { 228 // If this is an "unlike," we just need to add back the previous like entry and like 229 // like count. We do some sanity checks first though to be extra safe. 230 231 prevLikeEntry := utxoOpsForTxn[operationIndex].PrevLikeEntry 232 // Sanity check: verify that the user on the likeEntry matches the transaction sender. 233 if !reflect.DeepEqual(prevLikeEntry.LikerPubKey, currentTxn.PublicKey) { 234 return fmt.Errorf("_disconnectLike: User public key on "+ 235 "LikeEntry was %s but the PublicKey on the txn was %s", 236 PkToStringBoth(prevLikeEntry.LikerPubKey), 237 PkToStringBoth(currentTxn.PublicKey)) 238 } 239 240 // Sanity check: verify that the post hash on the prevLikeEntry matches the transaction's. 241 if !reflect.DeepEqual(prevLikeEntry.LikedPostHash, txMeta.LikedPostHash) { 242 return fmt.Errorf("_disconnectLike: Liked post hash on "+ 243 "LikeEntry was %s but the LikedPostHash on the txn was %s", 244 prevLikeEntry.LikedPostHash, txMeta.LikedPostHash) 245 } 246 247 // Set the like entry and like count to their previous state. 248 bav._setLikeEntryMappings(prevLikeEntry) 249 likedPostEntry.LikeCount = utxoOpsForTxn[operationIndex].PrevLikeCount 250 bav._setPostEntryMappings(likedPostEntry) 251 } else { 252 // If this is a normal "like," we do some sanity checks and then delete the entry. 253 254 // Get the LikeEntry. If we don't find it or isDeleted=true, that's an error. 255 likeKey := MakeLikeKey(currentTxn.PublicKey, *txMeta.LikedPostHash) 256 likeEntry := bav._getLikeEntryForLikeKey(&likeKey) 257 if likeEntry == nil || likeEntry.isDeleted { 258 return fmt.Errorf("_disconnectLike: LikeEntry for "+ 259 "likeKey %v was found to be nil or isDeleted not set appropriately: %v", 260 &likeKey, likeEntry) 261 } 262 263 // Sanity check: verify that the user on the likeEntry matches the transaction sender. 264 if !reflect.DeepEqual(likeEntry.LikerPubKey, currentTxn.PublicKey) { 265 return fmt.Errorf("_disconnectLike: User public key on "+ 266 "LikeEntry was %s but the PublicKey on the txn was %s", 267 PkToStringBoth(likeEntry.LikerPubKey), 268 PkToStringBoth(currentTxn.PublicKey)) 269 } 270 271 // Sanity check: verify that the post hash on the likeEntry matches the transaction's. 272 if !reflect.DeepEqual(likeEntry.LikedPostHash, txMeta.LikedPostHash) { 273 return fmt.Errorf("_disconnectLike: Liked post hash on "+ 274 "LikeEntry was %s but the LikedPostHash on the txn was %s", 275 likeEntry.LikedPostHash, txMeta.LikedPostHash) 276 } 277 278 // Now that we're confident the FollowEntry lines up with the transaction we're 279 // rolling back, delete the mappings and set the like counter to its previous value. 280 bav._deleteLikeEntryMappings(likeEntry) 281 likedPostEntry.LikeCount = utxoOpsForTxn[operationIndex].PrevLikeCount 282 bav._setPostEntryMappings(likedPostEntry) 283 } 284 285 // Now revert the basic transfer with the remaining operations. Cut off 286 // the Like operation at the end since we just reverted it. 287 return bav._disconnectBasicTransfer( 288 currentTxn, txnHash, utxoOpsForTxn[:operationIndex], blockHeight) 289 }