github.com/aakash4dev/cometbft@v0.38.2/state/txindex/kv/kv.go (about) 1 package kv 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "fmt" 8 "math/big" 9 "strconv" 10 "strings" 11 12 "github.com/aakash4dev/cometbft/libs/log" 13 14 "github.com/cosmos/gogoproto/proto" 15 16 dbm "github.com/aakash4dev/cometbft-db" 17 18 abci "github.com/aakash4dev/cometbft/abci/types" 19 idxutil "github.com/aakash4dev/cometbft/internal/indexer" 20 "github.com/aakash4dev/cometbft/libs/pubsub/query" 21 "github.com/aakash4dev/cometbft/libs/pubsub/query/syntax" 22 "github.com/aakash4dev/cometbft/state/indexer" 23 "github.com/aakash4dev/cometbft/state/txindex" 24 "github.com/aakash4dev/cometbft/types" 25 ) 26 27 const ( 28 tagKeySeparator = "/" 29 eventSeqSeparator = "$es$" 30 ) 31 32 var _ txindex.TxIndexer = (*TxIndex)(nil) 33 34 // TxIndex is the simplest possible indexer, backed by key-value storage (levelDB). 35 type TxIndex struct { 36 store dbm.DB 37 // Number the events in the event list 38 eventSeq int64 39 40 log log.Logger 41 } 42 43 // NewTxIndex creates new KV indexer. 44 func NewTxIndex(store dbm.DB) *TxIndex { 45 return &TxIndex{ 46 store: store, 47 } 48 } 49 50 func (txi *TxIndex) SetLogger(l log.Logger) { 51 txi.log = l 52 } 53 54 // Get gets transaction from the TxIndex storage and returns it or nil if the 55 // transaction is not found. 56 func (txi *TxIndex) Get(hash []byte) (*abci.TxResult, error) { 57 if len(hash) == 0 { 58 return nil, txindex.ErrorEmptyHash 59 } 60 61 rawBytes, err := txi.store.Get(hash) 62 if err != nil { 63 panic(err) 64 } 65 if rawBytes == nil { 66 return nil, nil 67 } 68 69 txResult := new(abci.TxResult) 70 err = proto.Unmarshal(rawBytes, txResult) 71 if err != nil { 72 return nil, fmt.Errorf("error reading TxResult: %v", err) 73 } 74 75 return txResult, nil 76 } 77 78 // AddBatch indexes a batch of transactions using the given list of events. Each 79 // key that indexed from the tx's events is a composite of the event type and 80 // the respective attribute's key delimited by a "." (eg. "account.number"). 81 // Any event with an empty type is not indexed. 82 func (txi *TxIndex) AddBatch(b *txindex.Batch) error { 83 storeBatch := txi.store.NewBatch() 84 defer storeBatch.Close() 85 86 for _, result := range b.Ops { 87 hash := types.Tx(result.Tx).Hash() 88 89 // index tx by events 90 err := txi.indexEvents(result, hash, storeBatch) 91 if err != nil { 92 return err 93 } 94 95 // index by height (always) 96 err = storeBatch.Set(keyForHeight(result), hash) 97 if err != nil { 98 return err 99 } 100 101 rawBytes, err := proto.Marshal(result) 102 if err != nil { 103 return err 104 } 105 // index by hash (always) 106 err = storeBatch.Set(hash, rawBytes) 107 if err != nil { 108 return err 109 } 110 } 111 112 return storeBatch.WriteSync() 113 } 114 115 // Index indexes a single transaction using the given list of events. Each key 116 // that indexed from the tx's events is a composite of the event type and the 117 // respective attribute's key delimited by a "." (eg. "account.number"). 118 // Any event with an empty type is not indexed. 119 // 120 // If a transaction is indexed with the same hash as a previous transaction, it will 121 // be overwritten unless the tx result was NOT OK and the prior result was OK i.e. 122 // more transactions that successfully executed overwrite transactions that failed 123 // or successful yet older transactions. 124 func (txi *TxIndex) Index(result *abci.TxResult) error { 125 b := txi.store.NewBatch() 126 defer b.Close() 127 128 hash := types.Tx(result.Tx).Hash() 129 130 if !result.Result.IsOK() { 131 oldResult, err := txi.Get(hash) 132 if err != nil { 133 return err 134 } 135 136 // if the new transaction failed and it's already indexed in an older block and was successful 137 // we skip it as we want users to get the older successful transaction when they query. 138 if oldResult != nil && oldResult.Result.Code == abci.CodeTypeOK { 139 return nil 140 } 141 } 142 143 // index tx by events 144 err := txi.indexEvents(result, hash, b) 145 if err != nil { 146 return err 147 } 148 149 // index by height (always) 150 err = b.Set(keyForHeight(result), hash) 151 if err != nil { 152 return err 153 } 154 155 rawBytes, err := proto.Marshal(result) 156 if err != nil { 157 return err 158 } 159 // index by hash (always) 160 err = b.Set(hash, rawBytes) 161 if err != nil { 162 return err 163 } 164 165 return b.WriteSync() 166 } 167 168 func (txi *TxIndex) indexEvents(result *abci.TxResult, hash []byte, store dbm.Batch) error { 169 for _, event := range result.Result.Events { 170 txi.eventSeq = txi.eventSeq + 1 171 // only index events with a non-empty type 172 if len(event.Type) == 0 { 173 continue 174 } 175 176 for _, attr := range event.Attributes { 177 if len(attr.Key) == 0 { 178 continue 179 } 180 181 // index if `index: true` is set 182 compositeTag := fmt.Sprintf("%s.%s", event.Type, attr.Key) 183 // ensure event does not conflict with a reserved prefix key 184 if compositeTag == types.TxHashKey || compositeTag == types.TxHeightKey { 185 return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeTag) 186 } 187 if attr.GetIndex() { 188 err := store.Set(keyForEvent(compositeTag, attr.Value, result, txi.eventSeq), hash) 189 if err != nil { 190 return err 191 } 192 } 193 } 194 } 195 196 return nil 197 } 198 199 // Search performs a search using the given query. 200 // 201 // It breaks the query into conditions (like "tx.height > 5"). For each 202 // condition, it queries the DB index. One special use cases here: (1) if 203 // "tx.hash" is found, it returns tx result for it (2) for range queries it is 204 // better for the client to provide both lower and upper bounds, so we are not 205 // performing a full scan. Results from querying indexes are then intersected 206 // and returned to the caller, in no particular order. 207 // 208 // Search will exit early and return any result fetched so far, 209 // when a message is received on the context chan. 210 func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*abci.TxResult, error) { 211 select { 212 case <-ctx.Done(): 213 return make([]*abci.TxResult, 0), nil 214 215 default: 216 } 217 218 var hashesInitialized bool 219 filteredHashes := make(map[string][]byte) 220 221 // get a list of conditions (like "tx.height > 5") 222 conditions := q.Syntax() 223 224 // if there is a hash condition, return the result immediately 225 hash, ok, err := lookForHash(conditions) 226 if err != nil { 227 return nil, fmt.Errorf("error during searching for a hash in the query: %w", err) 228 } else if ok { 229 res, err := txi.Get(hash) 230 switch { 231 case err != nil: 232 return []*abci.TxResult{}, fmt.Errorf("error while retrieving the result: %w", err) 233 case res == nil: 234 return []*abci.TxResult{}, nil 235 default: 236 return []*abci.TxResult{res}, nil 237 } 238 } 239 240 // conditions to skip because they're handled before "everything else" 241 skipIndexes := make([]int, 0) 242 var heightInfo HeightInfo 243 244 // If we are not matching events and tx.height = 3 occurs more than once, the later value will 245 // overwrite the first one. 246 conditions, heightInfo = dedupHeight(conditions) 247 248 if !heightInfo.onlyHeightEq { 249 skipIndexes = append(skipIndexes, heightInfo.heightEqIdx) 250 } 251 252 // extract ranges 253 // if both upper and lower bounds exist, it's better to get them in order not 254 // no iterate over kvs that are not within range. 255 ranges, rangeIndexes, heightRange := indexer.LookForRangesWithHeight(conditions) 256 heightInfo.heightRange = heightRange 257 if len(ranges) > 0 { 258 skipIndexes = append(skipIndexes, rangeIndexes...) 259 260 for _, qr := range ranges { 261 262 // If we have a query range over height and want to still look for 263 // specific event values we do not want to simply return all 264 // transactios in this height range. We remember the height range info 265 // and pass it on to match() to take into account when processing events. 266 if qr.Key == types.TxHeightKey && !heightInfo.onlyHeightRange { 267 continue 268 } 269 if !hashesInitialized { 270 filteredHashes = txi.matchRange(ctx, qr, startKey(qr.Key), filteredHashes, true, heightInfo) 271 hashesInitialized = true 272 273 // Ignore any remaining conditions if the first condition resulted 274 // in no matches (assuming implicit AND operand). 275 if len(filteredHashes) == 0 { 276 break 277 } 278 } else { 279 filteredHashes = txi.matchRange(ctx, qr, startKey(qr.Key), filteredHashes, false, heightInfo) 280 } 281 } 282 } 283 284 // if there is a height condition ("tx.height=3"), extract it 285 286 // for all other conditions 287 for i, c := range conditions { 288 if intInSlice(i, skipIndexes) { 289 continue 290 } 291 292 if !hashesInitialized { 293 filteredHashes = txi.match(ctx, c, startKeyForCondition(c, heightInfo.height), filteredHashes, true, heightInfo) 294 hashesInitialized = true 295 296 // Ignore any remaining conditions if the first condition resulted 297 // in no matches (assuming implicit AND operand). 298 if len(filteredHashes) == 0 { 299 break 300 } 301 } else { 302 filteredHashes = txi.match(ctx, c, startKeyForCondition(c, heightInfo.height), filteredHashes, false, heightInfo) 303 } 304 } 305 306 results := make([]*abci.TxResult, 0, len(filteredHashes)) 307 resultMap := make(map[string]struct{}) 308 RESULTS_LOOP: 309 for _, h := range filteredHashes { 310 311 res, err := txi.Get(h) 312 if err != nil { 313 return nil, fmt.Errorf("failed to get Tx{%X}: %w", h, err) 314 } 315 hashString := string(h) 316 if _, ok := resultMap[hashString]; !ok { 317 resultMap[hashString] = struct{}{} 318 results = append(results, res) 319 } 320 // Potentially exit early. 321 select { 322 case <-ctx.Done(): 323 break RESULTS_LOOP 324 default: 325 } 326 } 327 328 return results, nil 329 } 330 331 func lookForHash(conditions []syntax.Condition) (hash []byte, ok bool, err error) { 332 for _, c := range conditions { 333 if c.Tag == types.TxHashKey { 334 decoded, err := hex.DecodeString(c.Arg.Value()) 335 return decoded, true, err 336 } 337 } 338 return 339 } 340 341 func (txi *TxIndex) setTmpHashes(tmpHeights map[string][]byte, it dbm.Iterator) { 342 eventSeq := extractEventSeqFromKey(it.Key()) 343 tmpHeights[string(it.Value())+eventSeq] = it.Value() 344 } 345 346 // match returns all matching txs by hash that meet a given condition and start 347 // key. An already filtered result (filteredHashes) is provided such that any 348 // non-intersecting matches are removed. 349 // 350 // NOTE: filteredHashes may be empty if no previous condition has matched. 351 func (txi *TxIndex) match( 352 ctx context.Context, 353 c syntax.Condition, 354 startKeyBz []byte, 355 filteredHashes map[string][]byte, 356 firstRun bool, 357 heightInfo HeightInfo, 358 ) map[string][]byte { 359 // A previous match was attempted but resulted in no matches, so we return 360 // no matches (assuming AND operand). 361 if !firstRun && len(filteredHashes) == 0 { 362 return filteredHashes 363 } 364 365 tmpHashes := make(map[string][]byte) 366 367 switch { 368 case c.Op == syntax.TEq: 369 it, err := dbm.IteratePrefix(txi.store, startKeyBz) 370 if err != nil { 371 panic(err) 372 } 373 defer it.Close() 374 375 EQ_LOOP: 376 for ; it.Valid(); it.Next() { 377 378 // If we have a height range in a query, we need only transactions 379 // for this height 380 keyHeight, err := extractHeightFromKey(it.Key()) 381 if err != nil { 382 txi.log.Error("failure to parse height from key:", err) 383 continue 384 } 385 withinBounds, err := checkHeightConditions(heightInfo, keyHeight) 386 if err != nil { 387 txi.log.Error("failure checking for height bounds:", err) 388 continue 389 } 390 if !withinBounds { 391 continue 392 } 393 txi.setTmpHashes(tmpHashes, it) 394 // Potentially exit early. 395 select { 396 case <-ctx.Done(): 397 break EQ_LOOP 398 default: 399 } 400 } 401 if err := it.Error(); err != nil { 402 panic(err) 403 } 404 405 case c.Op == syntax.TExists: 406 // XXX: can't use startKeyBz here because c.Operand is nil 407 // (e.g. "account.owner/<nil>/" won't match w/ a single row) 408 it, err := dbm.IteratePrefix(txi.store, startKey(c.Tag)) 409 if err != nil { 410 panic(err) 411 } 412 defer it.Close() 413 414 EXISTS_LOOP: 415 for ; it.Valid(); it.Next() { 416 keyHeight, err := extractHeightFromKey(it.Key()) 417 if err != nil { 418 txi.log.Error("failure to parse height from key:", err) 419 continue 420 } 421 withinBounds, err := checkHeightConditions(heightInfo, keyHeight) 422 if err != nil { 423 txi.log.Error("failure checking for height bounds:", err) 424 continue 425 } 426 if !withinBounds { 427 continue 428 } 429 txi.setTmpHashes(tmpHashes, it) 430 431 // Potentially exit early. 432 select { 433 case <-ctx.Done(): 434 break EXISTS_LOOP 435 default: 436 } 437 } 438 if err := it.Error(); err != nil { 439 panic(err) 440 } 441 442 case c.Op == syntax.TContains: 443 // XXX: startKey does not apply here. 444 // For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an" 445 // we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/" 446 it, err := dbm.IteratePrefix(txi.store, startKey(c.Tag)) 447 if err != nil { 448 panic(err) 449 } 450 defer it.Close() 451 452 CONTAINS_LOOP: 453 for ; it.Valid(); it.Next() { 454 if !isTagKey(it.Key()) { 455 continue 456 } 457 458 if strings.Contains(extractValueFromKey(it.Key()), c.Arg.Value()) { 459 keyHeight, err := extractHeightFromKey(it.Key()) 460 if err != nil { 461 txi.log.Error("failure to parse height from key:", err) 462 continue 463 } 464 withinBounds, err := checkHeightConditions(heightInfo, keyHeight) 465 if err != nil { 466 txi.log.Error("failure checking for height bounds:", err) 467 continue 468 } 469 if !withinBounds { 470 continue 471 } 472 txi.setTmpHashes(tmpHashes, it) 473 } 474 475 // Potentially exit early. 476 select { 477 case <-ctx.Done(): 478 break CONTAINS_LOOP 479 default: 480 } 481 } 482 if err := it.Error(); err != nil { 483 panic(err) 484 } 485 default: 486 panic("other operators should be handled already") 487 } 488 489 if len(tmpHashes) == 0 || firstRun { 490 // Either: 491 // 492 // 1. Regardless if a previous match was attempted, which may have had 493 // results, but no match was found for the current condition, then we 494 // return no matches (assuming AND operand). 495 // 496 // 2. A previous match was not attempted, so we return all results. 497 return tmpHashes 498 } 499 500 // Remove/reduce matches in filteredHashes that were not found in this 501 // match (tmpHashes). 502 REMOVE_LOOP: 503 for k, v := range filteredHashes { 504 tmpHash := tmpHashes[k] 505 if tmpHash == nil || !bytes.Equal(tmpHash, v) { 506 delete(filteredHashes, k) 507 508 // Potentially exit early. 509 select { 510 case <-ctx.Done(): 511 break REMOVE_LOOP 512 default: 513 } 514 } 515 } 516 517 return filteredHashes 518 } 519 520 // matchRange returns all matching txs by hash that meet a given queryRange and 521 // start key. An already filtered result (filteredHashes) is provided such that 522 // any non-intersecting matches are removed. 523 // 524 // NOTE: filteredHashes may be empty if no previous condition has matched. 525 func (txi *TxIndex) matchRange( 526 ctx context.Context, 527 qr indexer.QueryRange, 528 startKey []byte, 529 filteredHashes map[string][]byte, 530 firstRun bool, 531 heightInfo HeightInfo, 532 ) map[string][]byte { 533 // A previous match was attempted but resulted in no matches, so we return 534 // no matches (assuming AND operand). 535 if !firstRun && len(filteredHashes) == 0 { 536 return filteredHashes 537 } 538 539 tmpHashes := make(map[string][]byte) 540 541 it, err := dbm.IteratePrefix(txi.store, startKey) 542 if err != nil { 543 panic(err) 544 } 545 defer it.Close() 546 547 LOOP: 548 for ; it.Valid(); it.Next() { 549 if !isTagKey(it.Key()) { 550 continue 551 } 552 553 if _, ok := qr.AnyBound().(*big.Float); ok { 554 v := new(big.Int) 555 v, ok := v.SetString(extractValueFromKey(it.Key()), 10) 556 var vF *big.Float 557 if !ok { 558 vF, _, err = big.ParseFloat(extractValueFromKey(it.Key()), 10, 125, big.ToNearestEven) 559 if err != nil { 560 continue LOOP 561 } 562 563 } 564 if qr.Key != types.TxHeightKey { 565 keyHeight, err := extractHeightFromKey(it.Key()) 566 if err != nil { 567 txi.log.Error("failure to parse height from key:", err) 568 continue 569 } 570 withinBounds, err := checkHeightConditions(heightInfo, keyHeight) 571 if err != nil { 572 txi.log.Error("failure checking for height bounds:", err) 573 continue 574 } 575 if !withinBounds { 576 continue 577 } 578 } 579 var withinBounds bool 580 var err error 581 if !ok { 582 withinBounds, err = idxutil.CheckBounds(qr, vF) 583 } else { 584 withinBounds, err = idxutil.CheckBounds(qr, v) 585 } 586 if err != nil { 587 txi.log.Error("failed to parse bounds:", err) 588 } else { 589 if withinBounds { 590 txi.setTmpHashes(tmpHashes, it) 591 } 592 } 593 594 // XXX: passing time in a ABCI Events is not yet implemented 595 // case time.Time: 596 // v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) 597 // if v == r.upperBound { 598 // break 599 // } 600 } 601 602 // Potentially exit early. 603 select { 604 case <-ctx.Done(): 605 break LOOP 606 default: 607 } 608 } 609 if err := it.Error(); err != nil { 610 panic(err) 611 } 612 613 if len(tmpHashes) == 0 || firstRun { 614 // Either: 615 // 616 // 1. Regardless if a previous match was attempted, which may have had 617 // results, but no match was found for the current condition, then we 618 // return no matches (assuming AND operand). 619 // 620 // 2. A previous match was not attempted, so we return all results. 621 return tmpHashes 622 } 623 624 // Remove/reduce matches in filteredHashes that were not found in this 625 // match (tmpHashes). 626 REMOVE_LOOP: 627 for k, v := range filteredHashes { 628 tmpHash := tmpHashes[k] 629 if tmpHash == nil || !bytes.Equal(tmpHashes[k], v) { 630 delete(filteredHashes, k) 631 632 // Potentially exit early. 633 select { 634 case <-ctx.Done(): 635 break REMOVE_LOOP 636 default: 637 } 638 } 639 } 640 641 return filteredHashes 642 } 643 644 // Keys 645 646 func isTagKey(key []byte) bool { 647 // Normally, if the event was indexed with an event sequence, the number of 648 // tags should 4. Alternatively it should be 3 if the event was not indexed 649 // with the corresponding event sequence. However, some attribute values in 650 // production can contain the tag separator. Therefore, the condition is >= 3. 651 numTags := strings.Count(string(key), tagKeySeparator) 652 return numTags >= 3 653 } 654 655 func extractHeightFromKey(key []byte) (int64, error) { 656 parts := strings.SplitN(string(key), tagKeySeparator, -1) 657 658 return strconv.ParseInt(parts[len(parts)-2], 10, 64) 659 } 660 func extractValueFromKey(key []byte) string { 661 keyString := string(key) 662 parts := strings.SplitN(keyString, tagKeySeparator, -1) 663 partsLen := len(parts) 664 value := strings.TrimPrefix(keyString, parts[0]+tagKeySeparator) 665 666 suffix := "" 667 suffixLen := 2 668 669 for i := 1; i <= suffixLen; i++ { 670 suffix = tagKeySeparator + parts[partsLen-i] + suffix 671 } 672 return strings.TrimSuffix(value, suffix) 673 674 } 675 676 func extractEventSeqFromKey(key []byte) string { 677 parts := strings.SplitN(string(key), tagKeySeparator, -1) 678 679 lastEl := parts[len(parts)-1] 680 681 if strings.Contains(lastEl, eventSeqSeparator) { 682 return strings.SplitN(lastEl, eventSeqSeparator, 2)[1] 683 } 684 return "0" 685 } 686 func keyForEvent(key string, value string, result *abci.TxResult, eventSeq int64) []byte { 687 return []byte(fmt.Sprintf("%s/%s/%d/%d%s", 688 key, 689 value, 690 result.Height, 691 result.Index, 692 eventSeqSeparator+strconv.FormatInt(eventSeq, 10), 693 )) 694 } 695 696 func keyForHeight(result *abci.TxResult) []byte { 697 return []byte(fmt.Sprintf("%s/%d/%d/%d%s", 698 types.TxHeightKey, 699 result.Height, 700 result.Height, 701 result.Index, 702 // Added to facilitate having the eventSeq in event keys 703 // Otherwise queries break expecting 5 entries 704 eventSeqSeparator+"0", 705 )) 706 } 707 708 func startKeyForCondition(c syntax.Condition, height int64) []byte { 709 if height > 0 { 710 return startKey(c.Tag, c.Arg.Value(), height) 711 } 712 return startKey(c.Tag, c.Arg.Value()) 713 } 714 715 func startKey(fields ...interface{}) []byte { 716 var b bytes.Buffer 717 for _, f := range fields { 718 b.Write([]byte(fmt.Sprintf("%v", f) + tagKeySeparator)) 719 } 720 return b.Bytes() 721 }