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  }