github.com/badrootd/celestia-core@v0.0.0-20240305091328-aa4207a4b25d/state/indexer/block/kv/kv.go (about) 1 package kv 2 3 import ( 4 "bytes" 5 "context" 6 "errors" 7 "fmt" 8 "sort" 9 "strconv" 10 "strings" 11 12 dbm "github.com/cometbft/cometbft-db" 13 "github.com/google/orderedcode" 14 15 abci "github.com/badrootd/celestia-core/abci/types" 16 "github.com/badrootd/celestia-core/libs/pubsub/query" 17 "github.com/badrootd/celestia-core/state/indexer" 18 "github.com/badrootd/celestia-core/types" 19 ) 20 21 var _ indexer.BlockIndexer = (*BlockerIndexer)(nil) 22 23 // BlockerIndexer implements a block indexer, indexing BeginBlock and EndBlock 24 // events with an underlying KV store. Block events are indexed by their height, 25 // such that matching search criteria returns the respective block height(s). 26 type BlockerIndexer struct { 27 store dbm.DB 28 29 // Add unique event identifier to use when querying 30 // Matching will be done both on height AND eventSeq 31 eventSeq int64 32 } 33 34 func New(store dbm.DB) *BlockerIndexer { 35 return &BlockerIndexer{ 36 store: store, 37 } 38 } 39 40 // Has returns true if the given height has been indexed. An error is returned 41 // upon database query failure. 42 func (idx *BlockerIndexer) Has(height int64) (bool, error) { 43 key, err := heightKey(height) 44 if err != nil { 45 return false, fmt.Errorf("failed to create block height index key: %w", err) 46 } 47 48 return idx.store.Has(key) 49 } 50 51 // Index indexes BeginBlock and EndBlock events for a given block by its height. 52 // The following is indexed: 53 // 54 // primary key: encode(block.height | height) => encode(height) 55 // BeginBlock events: encode(eventType.eventAttr|eventValue|height|begin_block) => encode(height) 56 // EndBlock events: encode(eventType.eventAttr|eventValue|height|end_block) => encode(height) 57 func (idx *BlockerIndexer) Index(bh types.EventDataNewBlockHeader) error { 58 batch := idx.store.NewBatch() 59 defer batch.Close() 60 61 height := bh.Header.Height 62 63 // 1. index by height 64 key, err := heightKey(height) 65 if err != nil { 66 return fmt.Errorf("failed to create block height index key: %w", err) 67 } 68 if err := batch.Set(key, int64ToBytes(height)); err != nil { 69 return err 70 } 71 72 // 2. index BeginBlock events 73 if err := idx.indexEvents(batch, bh.ResultBeginBlock.Events, "begin_block", height); err != nil { 74 return fmt.Errorf("failed to index BeginBlock events: %w", err) 75 } 76 77 // 3. index EndBlock events 78 if err := idx.indexEvents(batch, bh.ResultEndBlock.Events, "end_block", height); err != nil { 79 return fmt.Errorf("failed to index EndBlock events: %w", err) 80 } 81 82 return batch.WriteSync() 83 } 84 85 // Search performs a query for block heights that match a given BeginBlock 86 // and Endblock event search criteria. The given query can match against zero, 87 // one or more block heights. In the case of height queries, i.e. block.height=H, 88 // if the height is indexed, that height alone will be returned. An error and 89 // nil slice is returned. Otherwise, a non-nil slice and nil error is returned. 90 func (idx *BlockerIndexer) Search(ctx context.Context, q *query.Query) ([]int64, error) { 91 results := make([]int64, 0) 92 select { 93 case <-ctx.Done(): 94 return results, nil 95 96 default: 97 } 98 99 conditions, err := q.Conditions() 100 if err != nil { 101 return nil, fmt.Errorf("failed to parse query conditions: %w", err) 102 } 103 // conditions to skip because they're handled before "everything else" 104 skipIndexes := make([]int, 0) 105 106 var matchEvents bool 107 var matchEventIdx int 108 109 // If the match.events keyword is at the beginning of the query, we will only 110 // return heights where the conditions are true within the same event 111 // and set the matchEvents to true 112 conditions, matchEvents = dedupMatchEvents(conditions) 113 114 if matchEvents { 115 matchEventIdx = 0 116 skipIndexes = append(skipIndexes, matchEventIdx) 117 } 118 119 // If there is an exact height query, return the result immediately 120 // (if it exists). 121 var ok bool 122 var heightInfo HeightInfo 123 if matchEvents { 124 // If we are not matching events and block.height = 3 occurs more than once, the later value will 125 // overwrite the first one. For match.events it will create problems. If we have a height range, we 126 // should ignore height equality. 127 conditions, heightInfo, ok = dedupHeight(conditions) 128 } else { 129 heightInfo.height, ok, heightInfo.heightEqIdx = lookForHeight(conditions) 130 } 131 132 // Extract ranges. If both upper and lower bounds exist, it's better to get 133 // them in order as to not iterate over kvs that are not within range. 134 // If we have a query range over height and want to still look for 135 // specific event values we do not want to simply return all 136 // blocks in this height range. We remember the height range info 137 // and pass it on to match() to take into account when processing events. 138 ranges, rangeIndexes, heightRange := indexer.LookForRangesWithHeight(conditions) 139 heightInfo.heightRange = heightRange 140 141 // If we have additional constraints and want to query per event 142 // attributes, we cannot simply return all blocks for a height. 143 // But we remember the height we want to find and forward it to 144 // match(). If we only have the height constraint and match.events keyword 145 // in the query (the second part of the ||), we don't need to query 146 // per event conditions and return all events within the height range. 147 if ok && (!matchEvents || (matchEvents && heightInfo.onlyHeightEq)) { 148 ok, err := idx.Has(heightInfo.height) 149 if err != nil { 150 return nil, err 151 } 152 153 if ok { 154 return []int64{heightInfo.height}, nil 155 } 156 157 return results, nil 158 } 159 160 var heightsInitialized bool 161 filteredHeights := make(map[string][]byte) 162 if matchEvents && heightInfo.heightEqIdx != -1 { 163 skipIndexes = append(skipIndexes, heightInfo.heightEqIdx) 164 } 165 166 if len(ranges) > 0 { 167 skipIndexes = append(skipIndexes, rangeIndexes...) 168 169 for _, qr := range ranges { 170 if qr.Key == types.BlockHeightKey && matchEvents && !heightInfo.onlyHeightRange { 171 // If the query contains ranges other than the height then we need to treat the height 172 // range when querying the conditions of the other range. 173 // Otherwise we can just return all the blocks within the height range (as there is no 174 // additional constraint on events) 175 continue 176 177 } 178 prefix, err := orderedcode.Append(nil, qr.Key) 179 if err != nil { 180 return nil, fmt.Errorf("failed to create prefix key: %w", err) 181 } 182 183 if !heightsInitialized { 184 filteredHeights, err = idx.matchRange(ctx, qr, prefix, filteredHeights, true, matchEvents, heightInfo) 185 if err != nil { 186 return nil, err 187 } 188 189 heightsInitialized = true 190 191 // Ignore any remaining conditions if the first condition resulted in no 192 // matches (assuming implicit AND operand). 193 if len(filteredHeights) == 0 { 194 break 195 } 196 } else { 197 filteredHeights, err = idx.matchRange(ctx, qr, prefix, filteredHeights, false, matchEvents, heightInfo) 198 if err != nil { 199 return nil, err 200 } 201 } 202 } 203 } 204 205 // for all other conditions 206 for i, c := range conditions { 207 if intInSlice(i, skipIndexes) { 208 continue 209 } 210 211 startKey, err := orderedcode.Append(nil, c.CompositeKey, fmt.Sprintf("%v", c.Operand)) 212 213 if err != nil { 214 return nil, err 215 } 216 217 if !heightsInitialized { 218 filteredHeights, err = idx.match(ctx, c, startKey, filteredHeights, true, matchEvents, heightInfo) 219 if err != nil { 220 return nil, err 221 } 222 223 heightsInitialized = true 224 225 // Ignore any remaining conditions if the first condition resulted in no 226 // matches (assuming implicit AND operand). 227 if len(filteredHeights) == 0 { 228 break 229 } 230 } else { 231 filteredHeights, err = idx.match(ctx, c, startKey, filteredHeights, false, matchEvents, heightInfo) 232 if err != nil { 233 return nil, err 234 } 235 } 236 } 237 238 // fetch matching heights 239 results = make([]int64, 0, len(filteredHeights)) 240 resultMap := make(map[int64]struct{}) 241 for _, hBz := range filteredHeights { 242 h := int64FromBytes(hBz) 243 244 ok, err := idx.Has(h) 245 if err != nil { 246 return nil, err 247 } 248 _, okHeight := resultMap[h] 249 if ok && !okHeight { 250 resultMap[h] = struct{}{} 251 results = append(results, h) 252 } 253 254 select { 255 case <-ctx.Done(): 256 break 257 258 default: 259 } 260 } 261 262 sort.Slice(results, func(i, j int) bool { return results[i] < results[j] }) 263 264 return results, nil 265 } 266 267 // matchRange returns all matching block heights that match a given QueryRange 268 // and start key. An already filtered result (filteredHeights) is provided such 269 // that any non-intersecting matches are removed. 270 // 271 // NOTE: The provided filteredHeights may be empty if no previous condition has 272 // matched. 273 func (idx *BlockerIndexer) matchRange( 274 ctx context.Context, 275 qr indexer.QueryRange, 276 startKey []byte, 277 filteredHeights map[string][]byte, 278 firstRun bool, 279 matchEvents bool, 280 heightInfo HeightInfo, 281 ) (map[string][]byte, error) { 282 283 // A previous match was attempted but resulted in no matches, so we return 284 // no matches (assuming AND operand). 285 if !firstRun && len(filteredHeights) == 0 { 286 return filteredHeights, nil 287 } 288 289 tmpHeights := make(map[string][]byte) 290 291 it, err := dbm.IteratePrefix(idx.store, startKey) 292 if err != nil { 293 return nil, fmt.Errorf("failed to create prefix iterator: %w", err) 294 } 295 defer it.Close() 296 297 LOOP: 298 for ; it.Valid(); it.Next() { 299 var ( 300 eventValue string 301 err error 302 ) 303 304 if qr.Key == types.BlockHeightKey { 305 eventValue, err = parseValueFromPrimaryKey(it.Key()) 306 } else { 307 eventValue, err = parseValueFromEventKey(it.Key()) 308 } 309 310 if err != nil { 311 continue 312 } 313 314 if _, ok := qr.AnyBound().(int64); ok { 315 v, err := strconv.ParseInt(eventValue, 10, 64) 316 if err != nil { 317 continue LOOP 318 } 319 320 if matchEvents && qr.Key != types.BlockHeightKey { 321 keyHeight, err := parseHeightFromEventKey(it.Key()) 322 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 323 continue LOOP 324 } 325 } 326 if checkBounds(qr, v) { 327 idx.setTmpHeights(tmpHeights, it, matchEvents) 328 } 329 } 330 331 select { 332 case <-ctx.Done(): 333 break 334 335 default: 336 } 337 } 338 339 if err := it.Error(); err != nil { 340 return nil, err 341 } 342 343 if len(tmpHeights) == 0 || firstRun { 344 // Either: 345 // 346 // 1. Regardless if a previous match was attempted, which may have had 347 // results, but no match was found for the current condition, then we 348 // return no matches (assuming AND operand). 349 // 350 // 2. A previous match was not attempted, so we return all results. 351 return tmpHeights, nil 352 } 353 354 // Remove/reduce matches in filteredHashes that were not found in this 355 // match (tmpHashes). 356 for k, v := range filteredHeights { 357 tmpHeight := tmpHeights[k] 358 359 // Check whether in this iteration we have not found an overlapping height (tmpHeight == nil) 360 // or whether the events in which the attributed occurred do not match (first part of the condition) 361 if tmpHeight == nil || !bytes.Equal(tmpHeight, v) { 362 delete(filteredHeights, k) 363 364 select { 365 case <-ctx.Done(): 366 break 367 368 default: 369 } 370 } 371 } 372 373 return filteredHeights, nil 374 } 375 376 func (idx *BlockerIndexer) setTmpHeights(tmpHeights map[string][]byte, it dbm.Iterator, matchEvents bool) { 377 // If we return attributes that occur within the same events, then store the event sequence in the 378 // result map as well 379 if matchEvents { 380 eventSeq, _ := parseEventSeqFromEventKey(it.Key()) 381 retVal := it.Value() 382 tmpHeights[string(retVal)+strconv.FormatInt(eventSeq, 10)] = it.Value() 383 } else { 384 tmpHeights[string(it.Value())] = it.Value() 385 } 386 } 387 388 func checkBounds(ranges indexer.QueryRange, v int64) bool { 389 include := true 390 lowerBound := ranges.LowerBoundValue() 391 upperBound := ranges.UpperBoundValue() 392 if lowerBound != nil && v < lowerBound.(int64) { 393 include = false 394 } 395 396 if upperBound != nil && v > upperBound.(int64) { 397 include = false 398 } 399 400 return include 401 } 402 403 // match returns all matching heights that meet a given query condition and start 404 // key. An already filtered result (filteredHeights) is provided such that any 405 // non-intersecting matches are removed. 406 // 407 // NOTE: The provided filteredHeights may be empty if no previous condition has 408 // matched. 409 func (idx *BlockerIndexer) match( 410 ctx context.Context, 411 c query.Condition, 412 startKeyBz []byte, 413 filteredHeights map[string][]byte, 414 firstRun bool, 415 matchEvents bool, 416 heightInfo HeightInfo, 417 ) (map[string][]byte, error) { 418 419 // A previous match was attempted but resulted in no matches, so we return 420 // no matches (assuming AND operand). 421 if !firstRun && len(filteredHeights) == 0 { 422 return filteredHeights, nil 423 } 424 425 tmpHeights := make(map[string][]byte) 426 427 switch { 428 case c.Op == query.OpEqual: 429 it, err := dbm.IteratePrefix(idx.store, startKeyBz) 430 if err != nil { 431 return nil, fmt.Errorf("failed to create prefix iterator: %w", err) 432 } 433 defer it.Close() 434 435 for ; it.Valid(); it.Next() { 436 if matchEvents { 437 keyHeight, err := parseHeightFromEventKey(it.Key()) 438 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 439 continue 440 } 441 442 } 443 idx.setTmpHeights(tmpHeights, it, matchEvents) 444 445 if err := ctx.Err(); err != nil { 446 break 447 } 448 } 449 450 if err := it.Error(); err != nil { 451 return nil, err 452 } 453 454 case c.Op == query.OpExists: 455 prefix, err := orderedcode.Append(nil, c.CompositeKey) 456 if err != nil { 457 return nil, err 458 } 459 460 it, err := dbm.IteratePrefix(idx.store, prefix) 461 if err != nil { 462 return nil, fmt.Errorf("failed to create prefix iterator: %w", err) 463 } 464 defer it.Close() 465 466 for ; it.Valid(); it.Next() { 467 keyHeight, err := parseHeightFromEventKey(it.Key()) 468 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 469 continue 470 } 471 472 idx.setTmpHeights(tmpHeights, it, matchEvents) 473 474 select { 475 case <-ctx.Done(): 476 break 477 478 default: 479 } 480 } 481 482 if err := it.Error(); err != nil { 483 return nil, err 484 } 485 486 case c.Op == query.OpContains: 487 prefix, err := orderedcode.Append(nil, c.CompositeKey) 488 if err != nil { 489 return nil, err 490 } 491 492 it, err := dbm.IteratePrefix(idx.store, prefix) 493 if err != nil { 494 return nil, fmt.Errorf("failed to create prefix iterator: %w", err) 495 } 496 defer it.Close() 497 498 for ; it.Valid(); it.Next() { 499 eventValue, err := parseValueFromEventKey(it.Key()) 500 if err != nil { 501 continue 502 } 503 504 if strings.Contains(eventValue, c.Operand.(string)) { 505 keyHeight, err := parseHeightFromEventKey(it.Key()) 506 if err != nil || !checkHeightConditions(heightInfo, keyHeight) { 507 continue 508 } 509 idx.setTmpHeights(tmpHeights, it, matchEvents) 510 } 511 512 select { 513 case <-ctx.Done(): 514 break 515 516 default: 517 } 518 } 519 if err := it.Error(); err != nil { 520 return nil, err 521 } 522 523 default: 524 return nil, errors.New("other operators should be handled already") 525 } 526 527 if len(tmpHeights) == 0 || firstRun { 528 // Either: 529 // 530 // 1. Regardless if a previous match was attempted, which may have had 531 // results, but no match was found for the current condition, then we 532 // return no matches (assuming AND operand). 533 // 534 // 2. A previous match was not attempted, so we return all results. 535 return tmpHeights, nil 536 } 537 538 // Remove/reduce matches in filteredHeights that were not found in this 539 // match (tmpHeights). 540 for k, v := range filteredHeights { 541 tmpHeight := tmpHeights[k] 542 if tmpHeight == nil || !bytes.Equal(tmpHeight, v) { 543 delete(filteredHeights, k) 544 545 select { 546 case <-ctx.Done(): 547 break 548 549 default: 550 } 551 } 552 } 553 554 return filteredHeights, nil 555 } 556 557 func (idx *BlockerIndexer) indexEvents(batch dbm.Batch, events []abci.Event, typ string, height int64) error { 558 heightBz := int64ToBytes(height) 559 560 for _, event := range events { 561 idx.eventSeq = idx.eventSeq + 1 562 // only index events with a non-empty type 563 if len(event.Type) == 0 { 564 continue 565 } 566 567 for _, attr := range event.Attributes { 568 if len(attr.Key) == 0 { 569 continue 570 } 571 572 // index iff the event specified index:true and it's not a reserved event 573 compositeKey := fmt.Sprintf("%s.%s", event.Type, attr.Key) 574 if compositeKey == types.BlockHeightKey { 575 return fmt.Errorf("event type and attribute key \"%s\" is reserved; please use a different key", compositeKey) 576 } 577 578 if attr.GetIndex() { 579 key, err := eventKey(compositeKey, typ, attr.Value, height, idx.eventSeq) 580 if err != nil { 581 return fmt.Errorf("failed to create block index key: %w", err) 582 } 583 584 if err := batch.Set(key, heightBz); err != nil { 585 return err 586 } 587 } 588 } 589 } 590 591 return nil 592 }