github.com/deso-protocol/core@v1.2.9/lib/block_view_post.go (about) 1 package lib 2 3 import ( 4 "encoding/hex" 5 "fmt" 6 "github.com/btcsuite/btcd/btcec" 7 "github.com/davecgh/go-spew/spew" 8 "github.com/dgraph-io/badger/v3" 9 "github.com/golang/glog" 10 "github.com/pkg/errors" 11 "math" 12 "reflect" 13 "sort" 14 ) 15 16 func (bav *UtxoView) _getRepostEntryForRepostKey(repostKey *RepostKey) *RepostEntry { 17 // If an entry exists in the in-memory map, return the value of that mapping. 18 mapValue, existsMapValue := bav.RepostKeyToRepostEntry[*repostKey] 19 if existsMapValue { 20 return mapValue 21 } 22 23 // If we get here it means no value exists in our in-memory map. In this case, 24 // defer to the db. If a mapping exists in the db, return it. If not, return 25 // nil. Either way, save the value to the in-memory view mapping got later. 26 repostEntry := DbReposterPubKeyRepostedPostHashToRepostEntry( 27 bav.Handle, repostKey.ReposterPubKey[:], repostKey.RepostedPostHash) 28 if repostEntry != nil { 29 bav._setRepostEntryMappings(repostEntry) 30 } 31 return repostEntry 32 } 33 34 func (bav *UtxoView) _setRepostEntryMappings(repostEntry *RepostEntry) { 35 // This function shouldn't be called with nil. 36 if repostEntry == nil { 37 glog.Errorf("_setRepostEntryMappings: Called with nil RepostEntry; " + 38 "this should never happen.") 39 return 40 } 41 42 repostKey := MakeRepostKey(repostEntry.ReposterPubKey, *repostEntry.RepostedPostHash) 43 bav.RepostKeyToRepostEntry[repostKey] = repostEntry 44 } 45 46 func (bav *UtxoView) _deleteRepostEntryMappings(repostEntry *RepostEntry) { 47 48 if repostEntry == nil { 49 glog.Errorf("_deleteRepostEntryMappings: called with nil RepostEntry; " + 50 "this should never happen") 51 return 52 } 53 // Create a tombstone entry. 54 tombstoneRepostEntry := *repostEntry 55 tombstoneRepostEntry.isDeleted = true 56 57 // Set the mappings to point to the tombstone entry. 58 bav._setRepostEntryMappings(&tombstoneRepostEntry) 59 } 60 61 func (bav *UtxoView) _setDiamondEntryMappings(diamondEntry *DiamondEntry) { 62 // This function shouldn't be called with nil. 63 if diamondEntry == nil { 64 glog.Errorf("_setDiamondEntryMappings: Called with nil DiamondEntry; " + 65 "this should never happen.") 66 return 67 } 68 69 diamondKey := MakeDiamondKey( 70 diamondEntry.SenderPKID, diamondEntry.ReceiverPKID, diamondEntry.DiamondPostHash) 71 bav.DiamondKeyToDiamondEntry[diamondKey] = diamondEntry 72 } 73 74 func (bav *UtxoView) _deleteDiamondEntryMappings(diamondEntry *DiamondEntry) { 75 76 // Create a tombstone entry. 77 tombstoneDiamondEntry := *diamondEntry 78 tombstoneDiamondEntry.isDeleted = true 79 80 // Set the mappings to point to the tombstone entry. 81 bav._setDiamondEntryMappings(&tombstoneDiamondEntry) 82 } 83 84 func (bav *UtxoView) GetDiamondEntryForDiamondKey(diamondKey *DiamondKey) *DiamondEntry { 85 // If an entry exists in the in-memory map, return the value of that mapping. 86 bavDiamondEntry, existsMapValue := bav.DiamondKeyToDiamondEntry[*diamondKey] 87 if existsMapValue { 88 return bavDiamondEntry 89 } 90 91 // If we get here it means no value exists in our in-memory map. In this case, 92 // defer to the db. If a mapping exists in the db, return it. If not, return 93 // nil. 94 var diamondEntry *DiamondEntry 95 if bav.Postgres != nil { 96 diamond := bav.Postgres.GetDiamond(&diamondKey.SenderPKID, &diamondKey.ReceiverPKID, &diamondKey.DiamondPostHash) 97 if diamond != nil { 98 diamondEntry = &DiamondEntry{ 99 SenderPKID: diamond.SenderPKID, 100 ReceiverPKID: diamond.ReceiverPKID, 101 DiamondPostHash: diamond.DiamondPostHash, 102 DiamondLevel: int64(diamond.DiamondLevel), 103 } 104 } 105 } else { 106 diamondEntry = DbGetDiamondMappings(bav.Handle, &diamondKey.ReceiverPKID, &diamondKey.SenderPKID, &diamondKey.DiamondPostHash) 107 } 108 109 if diamondEntry != nil { 110 bav._setDiamondEntryMappings(diamondEntry) 111 } 112 113 return diamondEntry 114 } 115 116 func (bav *UtxoView) GetPostEntryForPostHash(postHash *BlockHash) *PostEntry { 117 // If an entry exists in the in-memory map, return the value of that mapping. 118 mapValue, existsMapValue := bav.PostHashToPostEntry[*postHash] 119 if existsMapValue { 120 return mapValue 121 } 122 123 // If we get here it means no value exists in our in-memory map. In this case, 124 // defer to the db. If a mapping exists in the db, return it. If not, return 125 // nil. 126 if bav.Postgres != nil { 127 post := bav.Postgres.GetPost(postHash) 128 if post != nil { 129 return bav.setPostMappings(post) 130 } 131 return nil 132 } else { 133 dbPostEntry := DBGetPostEntryByPostHash(bav.Handle, postHash) 134 if dbPostEntry != nil { 135 bav._setPostEntryMappings(dbPostEntry) 136 } 137 return dbPostEntry 138 } 139 } 140 141 func (bav *UtxoView) GetDiamondEntryMapForPublicKey(publicKey []byte, fetchYouDiamonded bool, 142 ) (_pkidToDiamondsMap map[PKID][]*DiamondEntry, _err error) { 143 pkidEntry := bav.GetPKIDForPublicKey(publicKey) 144 145 dbPKIDToDiamondsMap, err := DbGetPKIDsThatDiamondedYouMap(bav.Handle, pkidEntry.PKID, fetchYouDiamonded) 146 if err != nil { 147 return nil, errors.Wrapf(err, "GetDiamondEntryMapForPublicKey: Error Getting "+ 148 "PKIDs that diamonded you map from the DB.") 149 } 150 151 // Load all of the diamondEntries into the view. 152 for _, diamondEntryList := range dbPKIDToDiamondsMap { 153 for _, diamondEntry := range diamondEntryList { 154 diamondKey := &DiamondKey{ 155 SenderPKID: *diamondEntry.SenderPKID, 156 ReceiverPKID: *diamondEntry.ReceiverPKID, 157 DiamondPostHash: *diamondEntry.DiamondPostHash, 158 } 159 // If the diamond key is not in the view, add it to the view. 160 if _, ok := bav.DiamondKeyToDiamondEntry[*diamondKey]; !ok { 161 bav._setDiamondEntryMappings(diamondEntry) 162 } 163 } 164 } 165 166 // Iterate over all the diamondEntries in the view and build the final map. 167 pkidToDiamondsMap := make(map[PKID][]*DiamondEntry) 168 for _, diamondEntry := range bav.DiamondKeyToDiamondEntry { 169 if diamondEntry.isDeleted { 170 continue 171 } 172 // Make sure the diamondEntry we are looking at is for the correct receiver public key. 173 if !fetchYouDiamonded && reflect.DeepEqual(diamondEntry.ReceiverPKID, pkidEntry.PKID) { 174 pkidToDiamondsMap[*diamondEntry.SenderPKID] = append( 175 pkidToDiamondsMap[*diamondEntry.SenderPKID], diamondEntry) 176 } 177 178 // Make sure the diamondEntry we are looking at is for the correct sender public key. 179 if fetchYouDiamonded && reflect.DeepEqual(diamondEntry.SenderPKID, pkidEntry.PKID) { 180 pkidToDiamondsMap[*diamondEntry.ReceiverPKID] = append( 181 pkidToDiamondsMap[*diamondEntry.ReceiverPKID], diamondEntry) 182 } 183 } 184 185 return pkidToDiamondsMap, nil 186 } 187 188 func (bav *UtxoView) GetDiamondEntriesForSenderToReceiver(receiverPublicKey []byte, senderPublicKey []byte, 189 ) (_diamondEntries []*DiamondEntry, _err error) { 190 191 receiverPKIDEntry := bav.GetPKIDForPublicKey(receiverPublicKey) 192 senderPKIDEntry := bav.GetPKIDForPublicKey(senderPublicKey) 193 dbDiamondEntries, err := DbGetDiamondEntriesForSenderToReceiver(bav.Handle, receiverPKIDEntry.PKID, senderPKIDEntry.PKID) 194 if err != nil { 195 return nil, errors.Wrapf(err, "GetDiamondEntriesForGiverToReceiver: Error getting diamond entries from DB.") 196 } 197 198 // Load all of the diamondEntries into the view 199 for _, diamondEntry := range dbDiamondEntries { 200 diamondKey := &DiamondKey{ 201 SenderPKID: *diamondEntry.SenderPKID, 202 ReceiverPKID: *diamondEntry.ReceiverPKID, 203 DiamondPostHash: *diamondEntry.DiamondPostHash, 204 } 205 // If the diamond key is not in the view, add it to the view. 206 if _, ok := bav.DiamondKeyToDiamondEntry[*diamondKey]; !ok { 207 bav._setDiamondEntryMappings(diamondEntry) 208 } 209 } 210 211 var diamondEntries []*DiamondEntry 212 for _, diamondEntry := range bav.DiamondKeyToDiamondEntry { 213 if diamondEntry.isDeleted { 214 continue 215 } 216 217 // Make sure the diamondEntry we are looking at is for the correct sender and receiver pair 218 if reflect.DeepEqual(diamondEntry.ReceiverPKID, receiverPKIDEntry.PKID) && 219 reflect.DeepEqual(diamondEntry.SenderPKID, senderPKIDEntry.PKID) { 220 diamondEntries = append(diamondEntries, diamondEntry) 221 } 222 } 223 224 return diamondEntries, nil 225 } 226 227 func (bav *UtxoView) _setPostEntryMappings(postEntry *PostEntry) { 228 // This function shouldn't be called with nil. 229 if postEntry == nil { 230 glog.Errorf("_setPostEntryMappings: Called with nil PostEntry; this should never happen.") 231 return 232 } 233 234 // Add a mapping for the post. 235 bav.PostHashToPostEntry[*postEntry.PostHash] = postEntry 236 } 237 238 func (bav *UtxoView) _deletePostEntryMappings(postEntry *PostEntry) { 239 240 // Create a tombstone entry. 241 tombstonePostEntry := *postEntry 242 tombstonePostEntry.isDeleted = true 243 244 // Set the mappings to point to the tombstone entry. 245 bav._setPostEntryMappings(&tombstonePostEntry) 246 } 247 248 func (bav *UtxoView) setPostMappings(post *PGPost) *PostEntry { 249 postEntry := post.NewPostEntry() 250 251 // Add a mapping for the post. 252 bav.PostHashToPostEntry[*post.PostHash] = postEntry 253 254 return postEntry 255 } 256 257 func (bav *UtxoView) GetPostEntryReaderState( 258 readerPK []byte, postEntry *PostEntry) *PostEntryReaderState { 259 postEntryReaderState := &PostEntryReaderState{} 260 261 // Get like state. 262 postEntryReaderState.LikedByReader = bav.GetLikedByReader(readerPK, postEntry.PostHash) 263 264 // Get repost state. 265 postEntryReaderState.RepostPostHashHex, postEntryReaderState.RepostedByReader = bav.GetRepostPostEntryStateForReader(readerPK, postEntry.PostHash) 266 267 // Get diamond state. 268 senderPKID := bav.GetPKIDForPublicKey(readerPK) 269 receiverPKID := bav.GetPKIDForPublicKey(postEntry.PosterPublicKey) 270 if senderPKID == nil || receiverPKID == nil { 271 glog.V(1).Infof( 272 "GetPostEntryReaderState: Could not find PKID for reader PK: %s or poster PK: %s", 273 PkToString(readerPK, bav.Params), PkToString(postEntry.PosterPublicKey, bav.Params)) 274 } else { 275 diamondKey := MakeDiamondKey(senderPKID.PKID, receiverPKID.PKID, postEntry.PostHash) 276 diamondEntry := bav.GetDiamondEntryForDiamondKey(&diamondKey) 277 if diamondEntry != nil { 278 postEntryReaderState.DiamondLevelBestowed = diamondEntry.DiamondLevel 279 } 280 } 281 282 return postEntryReaderState 283 } 284 285 func (bav *UtxoView) GetRepostPostEntryStateForReader(readerPK []byte, postHash *BlockHash) (string, bool) { 286 repostKey := MakeRepostKey(readerPK, *postHash) 287 repostEntry := bav._getRepostEntryForRepostKey(&repostKey) 288 if repostEntry == nil { 289 return "", false 290 } 291 repostPostEntry := bav.GetPostEntryForPostHash(repostEntry.RepostPostHash) 292 if repostPostEntry == nil { 293 glog.Errorf("Could not find repost post entry from post hash: %v", repostEntry.RepostedPostHash) 294 return "", false 295 } 296 // We include the PostHashHex of this user's post that reposts the current post to 297 // handle undo-ing (AKA hiding) a repost. 298 // If the user's repost of this post is hidden, we set RepostedByReader to false. 299 return hex.EncodeToString(repostEntry.RepostPostHash[:]), !repostPostEntry.IsHidden 300 } 301 302 func (bav *UtxoView) GetCommentEntriesForParentStakeID(parentStakeID []byte) ([]*PostEntry, error) { 303 if bav.Postgres != nil { 304 posts := bav.Postgres.GetComments(NewBlockHash(parentStakeID)) 305 for _, post := range posts { 306 bav.setPostMappings(post) 307 } 308 } else { 309 _, dbCommentHashes, _, err := DBGetCommentPostHashesForParentStakeID(bav.Handle, parentStakeID, false) 310 if err != nil { 311 return nil, errors.Wrapf(err, "GetCommentEntriesForParentStakeID: Problem fetching comments: %v", err) 312 } 313 314 // Load comment hashes into the view. 315 for _, commentHash := range dbCommentHashes { 316 bav.GetPostEntryForPostHash(commentHash) 317 } 318 } 319 320 commentEntries := []*PostEntry{} 321 for _, postEntry := range bav.PostHashToPostEntry { 322 // Ignore deleted or rolled-back posts. 323 if postEntry.isDeleted { 324 continue 325 } 326 327 if len(postEntry.ParentStakeID) == 0 || !reflect.DeepEqual(postEntry.ParentStakeID, parentStakeID) { 328 continue // Skip posts that are not comments on the given parentStakeID. 329 } else { 330 // Add the comment to our map. 331 commentEntries = append(commentEntries, postEntry) 332 } 333 } 334 335 return commentEntries, nil 336 } 337 338 // Accepts a postEntry and returns as many parent posts as it can find up to maxDepth. 339 // This function never returns an error, only an empty list if it hits a non-post parentStakeID. 340 // If "rootFirst" is passed, the root of the tree will be returned first, not the 1st parent. 341 // _truncatedTree is a flag that is true when the root post was not reached before the maxDepth was hit. 342 func (bav *UtxoView) GetParentPostEntriesForPostEntry(postEntry *PostEntry, maxDepth uint32, rootFirst bool, 343 ) (_parentPostEntries []*PostEntry, _truncatedTree bool) { 344 345 parentStakeID := postEntry.ParentStakeID 346 parentPostEntries := []*PostEntry{} 347 348 // If the post passed has no parent or isn't a post, we return the empty list. 349 if len(parentStakeID) != HashSizeBytes { 350 return parentPostEntries, false 351 } 352 353 iterations := uint32(0) 354 for len(parentStakeID) == HashSizeBytes && iterations < maxDepth { 355 parentPostHash := &BlockHash{} 356 copy(parentPostHash[:], parentStakeID) 357 358 parentPostEntry := bav.GetPostEntryForPostHash(parentPostHash) 359 if postEntry == nil { 360 break 361 } 362 if rootFirst { 363 parentPostEntries = append([]*PostEntry{parentPostEntry}, parentPostEntries...) 364 } else { 365 parentPostEntries = append(parentPostEntries, parentPostEntry) 366 } 367 368 // Set up the next iteration of the loop. 369 parentStakeID = parentPostEntry.ParentStakeID 370 iterations += 1 371 } 372 373 return parentPostEntries, iterations >= maxDepth 374 } 375 376 // Just fetch all the posts from the db and join them with all the posts 377 // in the mempool. Then sort them by their timestamp. This can be called 378 // on an empty view or a view that already has a lot of transactions 379 // applied to it. 380 func (bav *UtxoView) GetAllPosts() (_corePosts []*PostEntry, _commentsByPostHash map[BlockHash][]*PostEntry, _err error) { 381 // Start by fetching all the posts we have in the db. 382 // 383 // TODO(performance): This currently fetches all posts. We should implement 384 // some kind of pagination instead though. 385 _, _, dbPostEntries, err := DBGetAllPostsByTstamp(bav.Handle, true /*fetchEntries*/) 386 if err != nil { 387 return nil, nil, errors.Wrapf(err, "GetAllPosts: Problem fetching PostEntry's from db: ") 388 } 389 390 // Iterate through the entries found in the db and force the view to load them. 391 // This fills in any gaps in the view so that, after this, the view should contain 392 // the union of what it had before plus what was in the db. 393 for _, dbPostEntry := range dbPostEntries { 394 bav.GetPostEntryForPostHash(dbPostEntry.PostHash) 395 } 396 397 // Do one more pass to load all the comments from the DB. 398 for _, postEntry := range bav.PostHashToPostEntry { 399 // Ignore deleted or rolled-back posts. 400 if postEntry.isDeleted { 401 continue 402 } 403 404 // If we have a post in the view and if that post is not a comment 405 // then fetch its attached comments from the db. We need to do this 406 // because the tstamp index above only fetches "core" posts not 407 // comments. 408 409 if len(postEntry.ParentStakeID) == 0 { 410 _, dbCommentHashes, _, err := DBGetCommentPostHashesForParentStakeID( 411 bav.Handle, postEntry.ParentStakeID, false /*fetchEntries*/) 412 if err != nil { 413 return nil, nil, errors.Wrapf(err, "GetAllPosts: Problem fetching comment PostEntry's from db: ") 414 } 415 for _, commentHash := range dbCommentHashes { 416 bav.GetPostEntryForPostHash(commentHash) 417 } 418 } 419 } 420 421 allCorePosts := []*PostEntry{} 422 commentsByPostHash := make(map[BlockHash][]*PostEntry) 423 for _, postEntry := range bav.PostHashToPostEntry { 424 // Ignore deleted or rolled-back posts. 425 if postEntry.isDeleted { 426 continue 427 } 428 429 // Every post is either a core post or a comment. If it has a stake ID 430 // its a comment, and if it doesn't then it's a core post. 431 if len(postEntry.ParentStakeID) == 0 { 432 allCorePosts = append(allCorePosts, postEntry) 433 } else { 434 // Add the comment to our map. 435 commentsForPost := commentsByPostHash[*NewBlockHash(postEntry.ParentStakeID)] 436 commentsForPost = append(commentsForPost, postEntry) 437 commentsByPostHash[*NewBlockHash(postEntry.ParentStakeID)] = commentsForPost 438 } 439 } 440 // Sort all the comment lists as well. Here we put the latest comment at the 441 // end. 442 for _, commentList := range commentsByPostHash { 443 sort.Slice(commentList, func(ii, jj int) bool { 444 return commentList[ii].TimestampNanos < commentList[jj].TimestampNanos 445 }) 446 } 447 448 return allCorePosts, commentsByPostHash, nil 449 } 450 451 func (bav *UtxoView) GetPostsPaginatedForPublicKeyOrderedByTimestamp(publicKey []byte, startPostHash *BlockHash, limit uint64, mediaRequired bool, nftRequired bool) (_posts []*PostEntry, _err error) { 452 if bav.Postgres != nil { 453 var startTime uint64 = math.MaxUint64 454 if startPostHash != nil { 455 startPostEntry := bav.GetPostEntryForPostHash(startPostHash) 456 startTime = startPostEntry.TimestampNanos 457 } 458 posts := bav.Postgres.GetPostsForPublicKey(publicKey, startTime, limit) 459 for _, post := range posts { 460 // TODO: Normalize this field so we get the correct number of results from the DB 461 if mediaRequired && !post.HasMedia() { 462 continue 463 } 464 // nftRequired set to determine if we only want posts that are NFTs 465 if nftRequired && !post.NFT { 466 continue 467 } 468 bav.setPostMappings(post) 469 } 470 } else { 471 handle := bav.Handle 472 dbPrefix := append([]byte{}, _PrefixPosterPublicKeyTimestampPostHash...) 473 dbPrefix = append(dbPrefix, publicKey...) 474 var prefix []byte 475 if startPostHash != nil { 476 startPostEntry := bav.GetPostEntryForPostHash(startPostHash) 477 if startPostEntry == nil { 478 return nil, fmt.Errorf("GetPostsPaginatedForPublicKeyOrderedByTimestamp: Invalid start post hash") 479 } 480 prefix = append(dbPrefix, EncodeUint64(startPostEntry.TimestampNanos)...) 481 prefix = append(prefix, startPostEntry.PostHash[:]...) 482 } else { 483 maxBigEndianUint64Bytes := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} 484 prefix = append(dbPrefix, maxBigEndianUint64Bytes...) 485 } 486 timestampSizeBytes := 8 487 var posts []*PostEntry 488 err := handle.View(func(txn *badger.Txn) error { 489 opts := badger.DefaultIteratorOptions 490 491 opts.PrefetchValues = false 492 493 // Go in reverse order 494 opts.Reverse = true 495 496 it := txn.NewIterator(opts) 497 defer it.Close() 498 it.Seek(prefix) 499 if startPostHash != nil { 500 // Skip the first post if we have a startPostHash. 501 it.Next() 502 } 503 for ; it.ValidForPrefix(dbPrefix) && uint64(len(posts)) < limit; it.Next() { 504 rawKey := it.Item().Key() 505 506 keyWithoutPrefix := rawKey[1:] 507 //posterPublicKey := keyWithoutPrefix[:HashSizeBytes] 508 publicKeySizeBytes := HashSizeBytes + 1 509 //tstampNanos := DecodeUint64(keyWithoutPrefix[publicKeySizeBytes:(publicKeySizeBytes + timestampSizeBytes)]) 510 511 postHash := &BlockHash{} 512 copy(postHash[:], keyWithoutPrefix[(publicKeySizeBytes+timestampSizeBytes):]) 513 postEntry := bav.GetPostEntryForPostHash(postHash) 514 if postEntry == nil { 515 return fmt.Errorf("Missing post entry") 516 } 517 if postEntry.isDeleted || postEntry.ParentStakeID != nil || postEntry.IsHidden { 518 continue 519 } 520 521 // mediaRequired set to determine if we only want posts that include media and ignore posts without 522 if mediaRequired && !postEntry.HasMedia() { 523 continue 524 } 525 526 // nftRequired set to determine if we only want posts that are NFTs 527 if nftRequired && !postEntry.IsNFT { 528 continue 529 } 530 531 posts = append(posts, postEntry) 532 } 533 return nil 534 }) 535 if err != nil { 536 return nil, err 537 } 538 } 539 540 var postEntries []*PostEntry 541 // Iterate over the view. Put all posts authored by the public key into our mempool posts slice 542 for _, postEntry := range bav.PostHashToPostEntry { 543 // Ignore deleted or hidden posts and any comments. 544 if postEntry.isDeleted || postEntry.IsHidden || len(postEntry.ParentStakeID) != 0 { 545 continue 546 } 547 548 // mediaRequired set to determine if we only want posts that include media and ignore posts without 549 if mediaRequired && !postEntry.HasMedia() { 550 continue 551 } 552 553 // nftRequired set to determine if we only want posts that are NFTs 554 if nftRequired && !postEntry.IsNFT { 555 continue 556 } 557 558 if reflect.DeepEqual(postEntry.PosterPublicKey, publicKey) { 559 postEntries = append(postEntries, postEntry) 560 } 561 } 562 563 return postEntries, nil 564 } 565 566 func (bav *UtxoView) GetDiamondSendersForPostHash(postHash *BlockHash) (_pkidToDiamondLevel map[PKID]int64, _err error) { 567 handle := bav.Handle 568 dbPrefix := append([]byte{}, _PrefixDiamondedPostHashDiamonderPKIDDiamondLevel...) 569 dbPrefix = append(dbPrefix, postHash[:]...) 570 keysFound, _ := EnumerateKeysForPrefix(handle, dbPrefix) 571 572 diamondPostEntry := bav.GetPostEntryForPostHash(postHash) 573 receiverPKIDEntry := bav.GetPKIDForPublicKey(diamondPostEntry.PosterPublicKey) 574 575 // Iterate over all the db keys & values and load them into the view. 576 expectedKeyLength := 1 + HashSizeBytes + btcec.PubKeyBytesLenCompressed + 8 577 for _, key := range keysFound { 578 // Sanity check that this is a reasonable key. 579 if len(key) != expectedKeyLength { 580 return nil, fmt.Errorf("UtxoView.GetDiamondsForPostHash: Invalid key length found: %d", len(key)) 581 } 582 583 senderPKID := &PKID{} 584 copy(senderPKID[:], key[1+HashSizeBytes:]) 585 586 diamondKey := &DiamondKey{ 587 SenderPKID: *senderPKID, 588 ReceiverPKID: *receiverPKIDEntry.PKID, 589 DiamondPostHash: *postHash, 590 } 591 592 bav.GetDiamondEntryForDiamondKey(diamondKey) 593 } 594 595 // Iterate over the view and create the final map to return. 596 pkidToDiamondLevel := make(map[PKID]int64) 597 for _, diamondEntry := range bav.DiamondKeyToDiamondEntry { 598 if !diamondEntry.isDeleted && reflect.DeepEqual(diamondEntry.DiamondPostHash[:], postHash[:]) { 599 pkidToDiamondLevel[*diamondEntry.SenderPKID] = diamondEntry.DiamondLevel 600 } 601 } 602 603 return pkidToDiamondLevel, nil 604 } 605 606 func (bav *UtxoView) GetRepostsForPostHash(postHash *BlockHash) (_reposterPubKeys [][]byte, _err error) { 607 handle := bav.Handle 608 dbPrefix := append([]byte{}, _PrefixRepostedPostHashReposterPubKey...) 609 dbPrefix = append(dbPrefix, postHash[:]...) 610 keysFound, _ := EnumerateKeysForPrefix(handle, dbPrefix) 611 612 // Iterate over all the db keys & values and load them into the view. 613 expectedKeyLength := 1 + HashSizeBytes + btcec.PubKeyBytesLenCompressed 614 for _, key := range keysFound { 615 // Sanity check that this is a reasonable key. 616 if len(key) != expectedKeyLength { 617 return nil, fmt.Errorf("UtxoView.GetRepostersForPostHash: Invalid key length found: %d", len(key)) 618 } 619 620 reposterPubKey := key[1+HashSizeBytes:] 621 622 repostKey := &RepostKey{ 623 ReposterPubKey: MakePkMapKey(reposterPubKey), 624 RepostedPostHash: *postHash, 625 } 626 627 bav._getRepostEntryForRepostKey(repostKey) 628 } 629 630 // Iterate over the view and create the final list to return. 631 reposterPubKeys := [][]byte{} 632 for _, repostEntry := range bav.RepostKeyToRepostEntry { 633 if !repostEntry.isDeleted && reflect.DeepEqual(repostEntry.RepostedPostHash[:], postHash[:]) { 634 reposterPubKeys = append(reposterPubKeys, repostEntry.ReposterPubKey) 635 } 636 } 637 638 return reposterPubKeys, nil 639 } 640 641 func (bav *UtxoView) GetQuoteRepostsForPostHash(postHash *BlockHash, 642 ) (_quoteReposterPubKeys [][]byte, _quoteReposterPubKeyToPosts map[PkMapKey][]*PostEntry, _err error) { 643 handle := bav.Handle 644 dbPrefix := append([]byte{}, _PrefixRepostedPostHashReposterPubKeyRepostPostHash...) 645 dbPrefix = append(dbPrefix, postHash[:]...) 646 keysFound, _ := EnumerateKeysForPrefix(handle, dbPrefix) 647 648 // Iterate over all the db keys & values and load them into the view. 649 expectedKeyLength := 1 + HashSizeBytes + btcec.PubKeyBytesLenCompressed + HashSizeBytes 650 651 repostPostHashIdx := 1 + HashSizeBytes + btcec.PubKeyBytesLenCompressed 652 for _, key := range keysFound { 653 // Sanity check that this is a reasonable key. 654 if len(key) != expectedKeyLength { 655 return nil, nil, fmt.Errorf("UtxoView.GetQuoteRepostsForPostHash: Invalid key length found: %d", len(key)) 656 } 657 658 repostPostHash := &BlockHash{} 659 copy(repostPostHash[:], key[repostPostHashIdx:]) 660 661 bav.GetPostEntryForPostHash(repostPostHash) 662 } 663 664 // Iterate over the view and create the final map to return. 665 quoteReposterPubKeys := [][]byte{} 666 quoteReposterPubKeyToPosts := make(map[PkMapKey][]*PostEntry) 667 668 for _, postEntry := range bav.PostHashToPostEntry { 669 if !postEntry.isDeleted && postEntry.IsQuotedRepost && reflect.DeepEqual(postEntry.RepostedPostHash[:], postHash[:]) { 670 quoteReposterPubKeys = append(quoteReposterPubKeys, postEntry.PosterPublicKey) 671 672 quoteRepostPosts, _ := quoteReposterPubKeyToPosts[MakePkMapKey(postEntry.PosterPublicKey)] 673 quoteRepostPosts = append(quoteRepostPosts, postEntry) 674 quoteReposterPubKeyToPosts[MakePkMapKey(postEntry.PosterPublicKey)] = quoteRepostPosts 675 } 676 } 677 678 return quoteReposterPubKeys, quoteReposterPubKeyToPosts, nil 679 } 680 681 func (bav *UtxoView) _connectSubmitPost( 682 txn *MsgDeSoTxn, txHash *BlockHash, blockHeight uint32, 683 verifySignatures bool, ignoreUtxos bool) ( 684 _totalInput uint64, _totalOutput uint64, _utxoOps []*UtxoOperation, _err error) { 685 686 // Check that the transaction has the right TxnType. 687 if txn.TxnMeta.GetTxnType() != TxnTypeSubmitPost { 688 return 0, 0, nil, fmt.Errorf("_connectSubmitPost: called with bad TxnType %s", 689 txn.TxnMeta.GetTxnType().String()) 690 } 691 txMeta := txn.TxnMeta.(*SubmitPostMetadata) 692 693 // Connect basic txn to get the total input and the total output without 694 // considering the transaction metadata. 695 // 696 // The ignoreUtxos flag is used to connect "seed" transactions when initializing 697 // the blockchain. It allows us to "seed" the database with posts and profiles 698 // when we do a hard fork, without having the transactions rejected due to their 699 // not being spendable. 700 var totalInput, totalOutput uint64 701 var utxoOpsForTxn = []*UtxoOperation{} 702 var err error 703 if !ignoreUtxos { 704 totalInput, totalOutput, utxoOpsForTxn, err = bav._connectBasicTransfer( 705 txn, txHash, blockHeight, verifySignatures) 706 if err != nil { 707 return 0, 0, nil, errors.Wrapf(err, "_connectSubmitPost: ") 708 } 709 710 // Force the input to be non-zero so that we can prevent replay attacks. 711 if totalInput == 0 { 712 return 0, 0, nil, RuleErrorSubmitPostRequiresNonZeroInput 713 } 714 } 715 716 // Transaction extra data contains both consensus-related, such as repost info, and additional information about a post, 717 // whereas PostExtraData is an attribute of a PostEntry that contains only non-consensus related 718 // information about a post, such as a link to a video that is embedded. 719 extraData := make(map[string][]byte) 720 for k, v := range txn.ExtraData { 721 extraData[k] = v 722 } 723 // Set the IsQuotedRepost attribute of postEntry based on extra data 724 isQuotedRepost := false 725 if quotedRepost, hasQuotedRepost := extraData[IsQuotedRepostKey]; hasQuotedRepost { 726 if reflect.DeepEqual(quotedRepost, QuotedRepostVal) { 727 isQuotedRepost = true 728 } 729 // Delete key since it is not needed in the PostExtraData map as IsQuotedRepost is involved in consensus code. 730 delete(extraData, IsQuotedRepostKey) 731 } 732 var repostedPostHash *BlockHash 733 if repostedPostHashBytes, isRepost := extraData[RepostedPostHash]; isRepost { 734 repostedPostHash = &BlockHash{} 735 copy(repostedPostHash[:], repostedPostHashBytes) 736 delete(extraData, RepostedPostHash) 737 } 738 739 // At this point the inputs and outputs have been processed. Now we 740 // need to handle the metadata. 741 742 // If the metadata has a PostHashToModify then treat it as modifying an 743 // existing post rather than creating a new post. 744 var prevPostEntry *PostEntry 745 var prevParentPostEntry *PostEntry 746 var prevGrandparentPostEntry *PostEntry 747 var prevRepostedPostEntry *PostEntry 748 var prevRepostEntry *RepostEntry 749 750 var newPostEntry *PostEntry 751 var newParentPostEntry *PostEntry 752 var newGrandparentPostEntry *PostEntry 753 var newRepostedPostEntry *PostEntry 754 var newRepostEntry *RepostEntry 755 if len(txMeta.PostHashToModify) != 0 { 756 // Make sure the post hash is valid 757 if len(txMeta.PostHashToModify) != HashSizeBytes { 758 return 0, 0, nil, errors.Wrapf( 759 RuleErrorSubmitPostInvalidPostHashToModify, 760 "_connectSubmitPost: Bad post hash: %#v", txMeta.PostHashToModify) 761 } 762 763 // Get the existing post entry, which must exist and be undeleted. 764 postHash := &BlockHash{} 765 copy(postHash[:], txMeta.PostHashToModify[:]) 766 existingPostEntryy := bav.GetPostEntryForPostHash(postHash) 767 if existingPostEntryy == nil || existingPostEntryy.isDeleted { 768 return 0, 0, nil, errors.Wrapf( 769 RuleErrorSubmitPostModifyingNonexistentPost, 770 "_connectSubmitPost: Post hash: %v", postHash) 771 } 772 773 // Post modification is only allowed by the original poster. 774 if !reflect.DeepEqual(txn.PublicKey, existingPostEntryy.PosterPublicKey) { 775 776 return 0, 0, nil, errors.Wrapf( 777 RuleErrorSubmitPostPostModificationNotAuthorized, 778 "_connectSubmitPost: Post hash: %v, poster public key: %v, "+ 779 "txn public key: %v, paramUpdater: %v", postHash, 780 PkToStringBoth(existingPostEntryy.PosterPublicKey), 781 PkToStringBoth(txn.PublicKey), spew.Sdump(bav.Params.ParamUpdaterPublicKeys)) 782 } 783 784 // Modification of an NFT is not allowed. 785 if existingPostEntryy.IsNFT { 786 return 0, 0, nil, errors.Wrapf(RuleErrorSubmitPostCannotUpdateNFT, "_connectSubmitPost: ") 787 } 788 789 // It's an error if we are updating the value of RepostedPostHash. A post can only ever repost a single post. 790 if !reflect.DeepEqual(repostedPostHash, existingPostEntryy.RepostedPostHash) { 791 return 0, 0, nil, errors.Wrapf( 792 RuleErrorSubmitPostUpdateRepostHash, 793 "_connectSubmitPost: cannot update reposted post hash when updating a post") 794 } 795 796 // It's an error if we are updating the value of IsQuotedRepost. 797 if isQuotedRepost != existingPostEntryy.IsQuotedRepost { 798 return 0, 0, nil, errors.Wrapf( 799 RuleErrorSubmitPostUpdateIsQuotedRepost, 800 "_connectSubmitPost: cannot update isQuotedRepost attribute of post when updating a post") 801 } 802 803 // Save the data from the post. Note that we don't make a deep copy 804 // because all the fields that we modify are non-pointer fields. 805 prevPostEntry = &PostEntry{} 806 *prevPostEntry = *existingPostEntryy 807 808 // Set the newPostEntry pointer to the existing entry 809 newPostEntry = existingPostEntryy 810 811 // The field values should have already been validated so set 812 // them. 813 if len(txMeta.Body) != 0 { 814 newPostEntry.Body = txMeta.Body 815 } 816 817 // Merge the remaining attributes of the transaction's ExtraData into the postEntry's PostExtraData map. 818 if len(extraData) > 0 { 819 newPostExtraData := make(map[string][]byte) 820 for k, v := range existingPostEntryy.PostExtraData { 821 newPostExtraData[k] = v 822 } 823 for k, v := range extraData { 824 // If we're given a value with length greater than 0, add it to the map. 825 if len(v) > 0 { 826 newPostExtraData[k] = v 827 } else { 828 // If the value we're given has a length of 0, this indicates that we should delete it if it exists. 829 delete(newPostExtraData, k) 830 } 831 } 832 newPostEntry.PostExtraData = newPostExtraData 833 } 834 // TODO: Right now a post can be undeleted by the owner of the post, 835 // which seems like undesired behavior if a paramUpdater is trying to reduce 836 // spam 837 newPostEntry.IsHidden = txMeta.IsHidden 838 839 // Obtain the parent posts 840 newParentPostEntry, newGrandparentPostEntry, err = bav._getParentAndGrandparentPostEntry(newPostEntry) 841 if err != nil { 842 return 0, 0, nil, errors.Wrapf(err, "_connectSubmitPost: error with _getParentAndGrandparentPostEntry: %v", postHash) 843 } 844 845 if newPostEntry.RepostedPostHash != nil { 846 newRepostedPostEntry = bav.GetPostEntryForPostHash(newPostEntry.RepostedPostHash) 847 } 848 849 // Figure out how much we need to change the parent / grandparent's comment count by 850 var commentCountUpdateAmount int 851 repostCountUpdateAmount := 0 852 quoteRepostCountUpdateAmount := 0 853 hidingPostEntry := !prevPostEntry.IsHidden && newPostEntry.IsHidden 854 if hidingPostEntry { 855 // If we're hiding a post then we need to decrement the comment count of the parent 856 // and grandparent posts. 857 commentCountUpdateAmount = -1 * int(1+prevPostEntry.CommentCount) 858 859 // If we're hiding a post that is a vanilla repost of another post, we decrement the repost count of the 860 // post that was reposted. 861 if IsVanillaRepost(newPostEntry) { 862 repostCountUpdateAmount = -1 863 } else if isQuotedRepost { 864 quoteRepostCountUpdateAmount = -1 865 } 866 } 867 868 unhidingPostEntry := prevPostEntry.IsHidden && !newPostEntry.IsHidden 869 if unhidingPostEntry { 870 // If we're unhiding a post then we need to increment the comment count of the parent 871 // and grandparent posts. 872 commentCountUpdateAmount = int(1 + prevPostEntry.CommentCount) 873 // If we are unhiding a post that is a vanilla repost of another post, we increment the repost count of 874 // the post that was reposted. 875 if IsVanillaRepost(newPostEntry) { 876 repostCountUpdateAmount = 1 877 } else if isQuotedRepost { 878 quoteRepostCountUpdateAmount = 1 879 } 880 } 881 882 // Save the data from the parent post. Note that we don't make a deep copy 883 // because all the fields that we modify are non-pointer fields. 884 if newParentPostEntry != nil { 885 prevParentPostEntry = &PostEntry{} 886 *prevParentPostEntry = *newParentPostEntry 887 bav._updateParentCommentCountForPost(newPostEntry, newParentPostEntry, commentCountUpdateAmount) 888 } 889 890 // Save the data from the grandparent post. Note that we don't make a deep copy 891 // because all the fields that we modify are non-pointer fields. 892 if newGrandparentPostEntry != nil { 893 prevGrandparentPostEntry = &PostEntry{} 894 *prevGrandparentPostEntry = *newGrandparentPostEntry 895 bav._updateParentCommentCountForPost(newPostEntry, newGrandparentPostEntry, commentCountUpdateAmount) 896 } 897 if newRepostedPostEntry != nil { 898 prevRepostedPostEntry = &PostEntry{} 899 *prevRepostedPostEntry = *newRepostedPostEntry 900 // If the previous post entry is a vanilla repost, we can set the prevRepostEntry. 901 if IsVanillaRepost(prevPostEntry) { 902 prevRepostKey := MakeRepostKey(prevPostEntry.PosterPublicKey, *prevPostEntry.RepostedPostHash) 903 prevRepostEntry = bav._getRepostEntryForRepostKey(&prevRepostKey) 904 if prevRepostEntry == nil { 905 return 0, 0, nil, fmt.Errorf("prevRepostEntry not found for prevPostEntry") 906 } 907 // Generally prevRepostEntry is identical to newRepostEntry. Currently, we enforce a check that 908 // the RepostedPostHash does not get modified when attempting to connect a submitPost transaction 909 newRepostEntry = &RepostEntry{ 910 ReposterPubKey: newPostEntry.PosterPublicKey, 911 RepostedPostHash: newPostEntry.RepostedPostHash, 912 RepostPostHash: newPostEntry.PostHash, 913 } 914 915 // Update the repost count if it has changed. 916 bav._updateRepostCount(newRepostedPostEntry, repostCountUpdateAmount) 917 } else { 918 // Update the quote repost count if it has changed. 919 bav._updateQuoteRepostCount(newRepostedPostEntry, quoteRepostCountUpdateAmount) 920 } 921 } 922 } else { 923 // In this case we are creating a post from scratch so validate 924 // all the fields. 925 926 // StakeMultipleBasisPoints > 0 < max 927 // Between 1x = 100% and 10x = 10,000% 928 if txMeta.StakeMultipleBasisPoints < 100*100 || 929 txMeta.StakeMultipleBasisPoints > bav.Params.MaxStakeMultipleBasisPoints { 930 931 return 0, 0, nil, errors.Wrapf(RuleErrorSubmitPostStakeMultipleSize, 932 "_connectSubmitPost: Invalid StakeMultipleSize: %d", 933 txMeta.StakeMultipleBasisPoints) 934 } 935 // CreatorBasisPoints > 0 < max 936 if txMeta.CreatorBasisPoints < 0 || 937 txMeta.CreatorBasisPoints > bav.Params.MaxCreatorBasisPoints { 938 939 return 0, 0, nil, errors.Wrapf(RuleErrorSubmitPostCreatorPercentageSize, 940 "_connectSubmitPost: Invalid CreatorPercentageSize: %d", 941 txMeta.CreatorBasisPoints) 942 } 943 // TstampNanos != 0 944 if txMeta.TimestampNanos == 0 { 945 return 0, 0, nil, errors.Wrapf(RuleErrorSubmitPostTimestampIsZero, 946 "_connectSubmitPost: Invalid Timestamp: %d", 947 txMeta.TimestampNanos) 948 } 949 // The parent stake id should be a block hash or profile public key if it's set. 950 if len(txMeta.ParentStakeID) != 0 && len(txMeta.ParentStakeID) != HashSizeBytes && 951 len(txMeta.ParentStakeID) != btcec.PubKeyBytesLenCompressed { 952 return 0, 0, nil, errors.Wrapf(RuleErrorSubmitPostInvalidParentStakeIDLength, 953 "_connectSubmitPost: Parent stake ID length %v must be either 0 or %v or %v", 954 len(txMeta.ParentStakeID), HashSizeBytes, btcec.PubKeyBytesLenCompressed) 955 } 956 957 // The PostHash is just the transaction hash. 958 postHash := txHash 959 existingPostEntry := bav.GetPostEntryForPostHash(postHash) 960 if existingPostEntry != nil && !existingPostEntry.isDeleted { 961 return 0, 0, nil, errors.Wrapf( 962 RuleErrorPostAlreadyExists, 963 "_connectSubmitPost: Post hash: %v", postHash) 964 } 965 966 if repostedPostHash != nil { 967 newRepostedPostEntry = bav.GetPostEntryForPostHash(repostedPostHash) 968 // It is an error if a post entry attempts to repost a post that does not exist. 969 if newRepostedPostEntry == nil { 970 return 0, 0, nil, RuleErrorSubmitPostRepostPostNotFound 971 } 972 // It is an error if a post is trying to repost a vanilla repost. 973 if IsVanillaRepost(newRepostedPostEntry) { 974 return 0, 0, nil, RuleErrorSubmitPostRepostOfRepost 975 } 976 } 977 978 // Set the post entry pointer to a brand new post. 979 newPostEntry = &PostEntry{ 980 PostHash: postHash, 981 PosterPublicKey: txn.PublicKey, 982 ParentStakeID: txMeta.ParentStakeID, 983 Body: txMeta.Body, 984 RepostedPostHash: repostedPostHash, 985 IsQuotedRepost: isQuotedRepost, 986 CreatorBasisPoints: txMeta.CreatorBasisPoints, 987 StakeMultipleBasisPoints: txMeta.StakeMultipleBasisPoints, 988 TimestampNanos: txMeta.TimestampNanos, 989 ConfirmationBlockHeight: blockHeight, 990 PostExtraData: extraData, 991 // Don't set IsHidden on new posts. 992 } 993 994 // Obtain the parent posts 995 newParentPostEntry, newGrandparentPostEntry, err = bav._getParentAndGrandparentPostEntry(newPostEntry) 996 if err != nil { 997 return 0, 0, nil, errors.Wrapf(err, "_connectSubmitPost: error with _getParentAndGrandparentPostEntry: %v", postHash) 998 } 999 1000 // Save the data from the parent posts. Note that we don't make a deep copy 1001 // because all the fields that we modify are non-pointer fields. 1002 if newParentPostEntry != nil { 1003 prevParentPostEntry = &PostEntry{} 1004 *prevParentPostEntry = *newParentPostEntry 1005 bav._updateParentCommentCountForPost(newPostEntry, newParentPostEntry, 1 /*amountToChangeParentBy*/) 1006 } 1007 1008 if newGrandparentPostEntry != nil { 1009 prevGrandparentPostEntry = &PostEntry{} 1010 *prevGrandparentPostEntry = *newGrandparentPostEntry 1011 bav._updateParentCommentCountForPost(newPostEntry, newGrandparentPostEntry, 1 /*amountToChangeParentBy*/) 1012 } 1013 1014 // Save the data from the reposted post. 1015 if newRepostedPostEntry != nil { 1016 prevRepostedPostEntry = &PostEntry{} 1017 *prevRepostedPostEntry = *newRepostedPostEntry 1018 1019 // We only set repost entry mappings and increment counts for vanilla reposts. 1020 if !isQuotedRepost { 1021 // Increment the repost count of the post that was reposted by 1 as we are creating a new 1022 // vanilla repost. 1023 bav._updateRepostCount(newRepostedPostEntry, 1) 1024 // Create the new repostEntry 1025 newRepostEntry = &RepostEntry{ 1026 ReposterPubKey: newPostEntry.PosterPublicKey, 1027 RepostedPostHash: newPostEntry.RepostedPostHash, 1028 RepostPostHash: newPostEntry.PostHash, 1029 } 1030 } else { 1031 // If it is a quote repost, we need to increment the corresponding count. 1032 bav._updateQuoteRepostCount(newRepostedPostEntry, 1) 1033 } 1034 } 1035 } 1036 1037 if verifySignatures { 1038 // _connectBasicTransfer has already checked that the transaction is 1039 // signed by the top-level public key, which we take to be the poster's 1040 // public key. 1041 } 1042 1043 // Set the mappings for the entry regardless of whether we modified it or 1044 // created it from scratch. 1045 bav._setPostEntryMappings(newPostEntry) 1046 if newParentPostEntry != nil { 1047 bav._setPostEntryMappings(newParentPostEntry) 1048 } 1049 if newGrandparentPostEntry != nil { 1050 bav._setPostEntryMappings(newGrandparentPostEntry) 1051 } 1052 if newRepostedPostEntry != nil { 1053 bav._setPostEntryMappings(newRepostedPostEntry) 1054 } 1055 1056 if newRepostEntry != nil { 1057 bav._setRepostEntryMappings(newRepostEntry) 1058 } 1059 1060 // Add an operation to the list at the end indicating we've added a post. 1061 utxoOpsForTxn = append(utxoOpsForTxn, &UtxoOperation{ 1062 // PrevPostEntry should generally be nil when we created a new post from 1063 // scratch, but non-nil if we modified an existing post. 1064 PrevPostEntry: prevPostEntry, 1065 PrevParentPostEntry: prevParentPostEntry, 1066 PrevGrandparentPostEntry: prevGrandparentPostEntry, 1067 PrevRepostedPostEntry: prevRepostedPostEntry, 1068 PrevRepostEntry: prevRepostEntry, 1069 Type: OperationTypeSubmitPost, 1070 }) 1071 1072 return totalInput, totalOutput, utxoOpsForTxn, nil 1073 } 1074 1075 func (bav *UtxoView) _getParentAndGrandparentPostEntry(postEntry *PostEntry) ( 1076 _parentPostEntry *PostEntry, _grandparentPostEntry *PostEntry, _err error) { 1077 var parentPostEntry *PostEntry 1078 var grandparentPostEntry *PostEntry 1079 1080 // This length check ensures that the parent is a post (and not something else, like a profile) 1081 // 1082 // If we ever allow commenting on something else such that the parent is not a post, but where 1083 // ParentStakeID is also HashSizeBytes, then this logic would likely need to be changed. 1084 if len(postEntry.ParentStakeID) == HashSizeBytes { 1085 parentPostEntry = bav.GetPostEntryForPostHash(NewBlockHash(postEntry.ParentStakeID)) 1086 if parentPostEntry == nil { 1087 return nil, nil, errors.Wrapf( 1088 RuleErrorSubmitPostParentNotFound, 1089 "_getParentAndGrandparentPostEntry: failed to find parent post for post hash: %v, parentStakeId: %v", 1090 postEntry.PostHash, hex.EncodeToString(postEntry.ParentStakeID), 1091 ) 1092 } 1093 } 1094 1095 if parentPostEntry != nil && len(parentPostEntry.ParentStakeID) == HashSizeBytes { 1096 grandparentPostEntry = bav.GetPostEntryForPostHash(NewBlockHash(parentPostEntry.ParentStakeID)) 1097 if grandparentPostEntry == nil { 1098 return nil, nil, errors.Wrapf( 1099 RuleErrorSubmitPostParentNotFound, 1100 "_getParentAndGrandparentPostEntry: failed to find grandparent post for post hash: %v, parentStakeId: %v, grandparentStakeId: %v", 1101 postEntry.PostHash, postEntry.ParentStakeID, parentPostEntry.ParentStakeID, 1102 ) 1103 } 1104 } 1105 1106 return parentPostEntry, grandparentPostEntry, nil 1107 } 1108 1109 // Adds amount to the repost count of the post at repostPostHash 1110 func (bav *UtxoView) _updateRepostCount(repostedPost *PostEntry, amount int) { 1111 result := int(repostedPost.RepostCount) + amount 1112 1113 // Repost count should never be below 0. 1114 if result < 0 { 1115 glog.Errorf("_updateRepostCountForPost: RepostCount < 0 for result %v, repost post hash: %v, amount : %v", 1116 result, repostedPost, amount) 1117 result = 0 1118 } 1119 repostedPost.RepostCount = uint64(result) 1120 1121 } 1122 1123 // Adds amount to the quote repost count of the post at repostPostHash 1124 func (bav *UtxoView) _updateQuoteRepostCount(repostedPost *PostEntry, amount int) { 1125 result := int(repostedPost.QuoteRepostCount) + amount 1126 1127 // Repost count should never be below 0. 1128 if result < 0 { 1129 glog.Errorf("_updateQuoteRepostCountForPost: QuoteRepostCount < 0 for result %v, repost post hash: %v, amount : %v", 1130 result, repostedPost, amount) 1131 result = 0 1132 } 1133 repostedPost.QuoteRepostCount = uint64(result) 1134 1135 } 1136 1137 func (bav *UtxoView) _updateParentCommentCountForPost(postEntry *PostEntry, parentPostEntry *PostEntry, amountToChangeParentBy int) { 1138 result := int(parentPostEntry.CommentCount) + amountToChangeParentBy 1139 if result < 0 { 1140 glog.Errorf("_updateParentCommentCountForPost: CommentCount < 0 for result %v, postEntry hash: %v, parentPostEntry hash: %v, amountToChangeParentBy: %v", 1141 result, postEntry.PostHash, parentPostEntry.PostHash, amountToChangeParentBy) 1142 result = 0 1143 } 1144 1145 parentPostEntry.CommentCount = uint64(result) 1146 } 1147 1148 func (bav *UtxoView) _disconnectSubmitPost( 1149 operationType OperationType, currentTxn *MsgDeSoTxn, txnHash *BlockHash, 1150 utxoOpsForTxn []*UtxoOperation, blockHeight uint32) error { 1151 1152 // Verify that the last operation is a SubmitPost operation 1153 if len(utxoOpsForTxn) == 0 { 1154 return fmt.Errorf("_disconnectSubmitPost: utxoOperations are missing") 1155 } 1156 operationIndex := len(utxoOpsForTxn) - 1 1157 currentOperation := utxoOpsForTxn[operationIndex] 1158 if currentOperation.Type != OperationTypeSubmitPost { 1159 return fmt.Errorf("_disconnectSubmitPost: Trying to revert "+ 1160 "OperationTypeSubmitPost but found type %v", 1161 currentOperation.Type) 1162 } 1163 1164 // Now we know the txMeta is SubmitPost 1165 txMeta := currentTxn.TxnMeta.(*SubmitPostMetadata) 1166 1167 // The post hash is either the transaction hash or the hash set 1168 // in the metadata. 1169 postHashModified := txnHash 1170 if len(txMeta.PostHashToModify) != 0 { 1171 postHashModified = &BlockHash{} 1172 copy(postHashModified[:], txMeta.PostHashToModify[:]) 1173 } 1174 1175 // Get the PostEntry. If we don't find 1176 // it or if it has isDeleted=true that's an error. 1177 postEntry := bav.GetPostEntryForPostHash(postHashModified) 1178 if postEntry == nil || postEntry.isDeleted { 1179 return fmt.Errorf("_disconnectSubmitPost: PostEntry for "+ 1180 "Post Hash %v was found to be nil or deleted: %v", 1181 &txnHash, postEntry) 1182 } 1183 1184 // Delete repost mappings if they exist. They will be added back later if there is a previous version of this 1185 // postEntry 1186 if IsVanillaRepost(postEntry) { 1187 repostKey := MakeRepostKey(postEntry.PosterPublicKey, *postEntry.RepostedPostHash) 1188 repostEntry := bav._getRepostEntryForRepostKey(&repostKey) 1189 if repostEntry == nil { 1190 return fmt.Errorf("_disconnectSubmitPost: RepostEntry for "+ 1191 "Post Has %v could not be found: %v", &txnHash, postEntry) 1192 } 1193 bav._deleteRepostEntryMappings(repostEntry) 1194 } 1195 1196 // Now that we are confident the PostEntry lines up with the transaction we're 1197 // rolling back, use the entry to delete the mappings for this post. 1198 // 1199 // Note: We don't need to delete the existing PostEntry mappings for parent and grandparent 1200 // before setting the prev mappings because the only thing that could have changed is the 1201 // comment count, which isn't indexed. 1202 bav._deletePostEntryMappings(postEntry) 1203 1204 // If we have a non-nil previous post entry then set the mappings for 1205 // that. 1206 if currentOperation.PrevPostEntry != nil { 1207 bav._setPostEntryMappings(currentOperation.PrevPostEntry) 1208 } 1209 if currentOperation.PrevParentPostEntry != nil { 1210 bav._setPostEntryMappings(currentOperation.PrevParentPostEntry) 1211 } 1212 if currentOperation.PrevGrandparentPostEntry != nil { 1213 bav._setPostEntryMappings(currentOperation.PrevGrandparentPostEntry) 1214 } 1215 if currentOperation.PrevRepostedPostEntry != nil { 1216 bav._setPostEntryMappings(currentOperation.PrevRepostedPostEntry) 1217 } 1218 if currentOperation.PrevRepostEntry != nil { 1219 bav._setRepostEntryMappings(currentOperation.PrevRepostEntry) 1220 } 1221 1222 // Now revert the basic transfer with the remaining operations. Cut off 1223 // the SubmitPost operation at the end since we just reverted it. 1224 return bav._disconnectBasicTransfer( 1225 currentTxn, txnHash, utxoOpsForTxn[:operationIndex], blockHeight) 1226 }