github.com/devwanda/aphelion-staking@v0.33.9/state/txindex/kv/kv.go (about) 1 package kv 2 3 import ( 4 "bytes" 5 "context" 6 "encoding/hex" 7 "fmt" 8 "strconv" 9 "strings" 10 "time" 11 12 "github.com/pkg/errors" 13 14 dbm "github.com/tendermint/tm-db" 15 16 "github.com/devwanda/aphelion-staking/libs/pubsub/query" 17 tmstring "github.com/devwanda/aphelion-staking/libs/strings" 18 "github.com/devwanda/aphelion-staking/state/txindex" 19 "github.com/devwanda/aphelion-staking/types" 20 ) 21 22 const ( 23 tagKeySeparator = "/" 24 ) 25 26 var _ txindex.TxIndexer = (*TxIndex)(nil) 27 28 // TxIndex is the simplest possible indexer, backed by key-value storage (levelDB). 29 type TxIndex struct { 30 store dbm.DB 31 compositeKeysToIndex []string 32 indexAllEvents bool 33 } 34 35 // NewTxIndex creates new KV indexer. 36 func NewTxIndex(store dbm.DB, options ...func(*TxIndex)) *TxIndex { 37 txi := &TxIndex{store: store, compositeKeysToIndex: make([]string, 0), indexAllEvents: false} 38 for _, o := range options { 39 o(txi) 40 } 41 return txi 42 } 43 44 // IndexEvents is an option for setting which composite keys to index. 45 func IndexEvents(compositeKeys []string) func(*TxIndex) { 46 return func(txi *TxIndex) { 47 txi.compositeKeysToIndex = compositeKeys 48 } 49 } 50 51 // IndexAllEvents is an option for indexing all events. 52 func IndexAllEvents() func(*TxIndex) { 53 return func(txi *TxIndex) { 54 txi.indexAllEvents = true 55 } 56 } 57 58 // Get gets transaction from the TxIndex storage and returns it or nil if the 59 // transaction is not found. 60 func (txi *TxIndex) Get(hash []byte) (*types.TxResult, error) { 61 if len(hash) == 0 { 62 return nil, txindex.ErrorEmptyHash 63 } 64 65 rawBytes, err := txi.store.Get(hash) 66 if err != nil { 67 panic(err) 68 } 69 if rawBytes == nil { 70 return nil, nil 71 } 72 73 txResult := new(types.TxResult) 74 err = cdc.UnmarshalBinaryBare(rawBytes, &txResult) 75 if err != nil { 76 return nil, fmt.Errorf("error reading TxResult: %v", err) 77 } 78 79 return txResult, nil 80 } 81 82 // AddBatch indexes a batch of transactions using the given list of events. Each 83 // key that indexed from the tx's events is a composite of the event type and 84 // the respective attribute's key delimited by a "." (eg. "account.number"). 85 // Any event with an empty type is not indexed. 86 func (txi *TxIndex) AddBatch(b *txindex.Batch) error { 87 storeBatch := txi.store.NewBatch() 88 defer storeBatch.Close() 89 90 for _, result := range b.Ops { 91 hash := result.Tx.Hash() 92 93 // index tx by events 94 txi.indexEvents(result, hash, storeBatch) 95 96 // index tx by height 97 if txi.indexAllEvents || tmstring.StringInSlice(types.TxHeightKey, txi.compositeKeysToIndex) { 98 storeBatch.Set(keyForHeight(result), hash) 99 } 100 101 // index tx by hash 102 rawBytes, err := cdc.MarshalBinaryBare(result) 103 if err != nil { 104 return err 105 } 106 storeBatch.Set(hash, rawBytes) 107 } 108 109 storeBatch.WriteSync() 110 return nil 111 } 112 113 // Index indexes a single transaction using the given list of events. Each key 114 // that indexed from the tx's events is a composite of the event type and the 115 // respective attribute's key delimited by a "." (eg. "account.number"). 116 // Any event with an empty type is not indexed. 117 func (txi *TxIndex) Index(result *types.TxResult) error { 118 b := txi.store.NewBatch() 119 defer b.Close() 120 121 hash := result.Tx.Hash() 122 123 // index tx by events 124 txi.indexEvents(result, hash, b) 125 126 // index tx by height 127 if txi.indexAllEvents || tmstring.StringInSlice(types.TxHeightKey, txi.compositeKeysToIndex) { 128 b.Set(keyForHeight(result), hash) 129 } 130 131 // index tx by hash 132 rawBytes, err := cdc.MarshalBinaryBare(result) 133 if err != nil { 134 return err 135 } 136 137 b.Set(hash, rawBytes) 138 b.WriteSync() 139 140 return nil 141 } 142 143 func (txi *TxIndex) indexEvents(result *types.TxResult, hash []byte, store dbm.SetDeleter) { 144 for _, event := range result.Result.Events { 145 // only index events with a non-empty type 146 if len(event.Type) == 0 { 147 continue 148 } 149 150 for _, attr := range event.Attributes { 151 if len(attr.Key) == 0 { 152 continue 153 } 154 155 compositeTag := fmt.Sprintf("%s.%s", event.Type, string(attr.Key)) 156 if txi.indexAllEvents || tmstring.StringInSlice(compositeTag, txi.compositeKeysToIndex) { 157 store.Set(keyForEvent(compositeTag, attr.Value, result), hash) 158 } 159 } 160 } 161 } 162 163 // Search performs a search using the given query. 164 // 165 // It breaks the query into conditions (like "tx.height > 5"). For each 166 // condition, it queries the DB index. One special use cases here: (1) if 167 // "tx.hash" is found, it returns tx result for it (2) for range queries it is 168 // better for the client to provide both lower and upper bounds, so we are not 169 // performing a full scan. Results from querying indexes are then intersected 170 // and returned to the caller, in no particular order. 171 // 172 // Search will exit early and return any result fetched so far, 173 // when a message is received on the context chan. 174 func (txi *TxIndex) Search(ctx context.Context, q *query.Query) ([]*types.TxResult, error) { 175 // Potentially exit early. 176 select { 177 case <-ctx.Done(): 178 results := make([]*types.TxResult, 0) 179 return results, nil 180 default: 181 } 182 183 var hashesInitialized bool 184 filteredHashes := make(map[string][]byte) 185 186 // get a list of conditions (like "tx.height > 5") 187 conditions, err := q.Conditions() 188 if err != nil { 189 return nil, errors.Wrap(err, "error during parsing conditions from query") 190 } 191 192 // if there is a hash condition, return the result immediately 193 hash, ok, err := lookForHash(conditions) 194 if err != nil { 195 return nil, errors.Wrap(err, "error during searching for a hash in the query") 196 } else if ok { 197 res, err := txi.Get(hash) 198 switch { 199 case err != nil: 200 return []*types.TxResult{}, errors.Wrap(err, "error while retrieving the result") 201 case res == nil: 202 return []*types.TxResult{}, nil 203 default: 204 return []*types.TxResult{res}, nil 205 } 206 } 207 208 // conditions to skip because they're handled before "everything else" 209 skipIndexes := make([]int, 0) 210 211 // extract ranges 212 // if both upper and lower bounds exist, it's better to get them in order not 213 // no iterate over kvs that are not within range. 214 ranges, rangeIndexes := lookForRanges(conditions) 215 if len(ranges) > 0 { 216 skipIndexes = append(skipIndexes, rangeIndexes...) 217 218 for _, r := range ranges { 219 if !hashesInitialized { 220 filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, true) 221 hashesInitialized = true 222 223 // Ignore any remaining conditions if the first condition resulted 224 // in no matches (assuming implicit AND operand). 225 if len(filteredHashes) == 0 { 226 break 227 } 228 } else { 229 filteredHashes = txi.matchRange(ctx, r, startKey(r.key), filteredHashes, false) 230 } 231 } 232 } 233 234 // if there is a height condition ("tx.height=3"), extract it 235 height := lookForHeight(conditions) 236 237 // for all other conditions 238 for i, c := range conditions { 239 if intInSlice(i, skipIndexes) { 240 continue 241 } 242 243 if !hashesInitialized { 244 filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, true) 245 hashesInitialized = true 246 247 // Ignore any remaining conditions if the first condition resulted 248 // in no matches (assuming implicit AND operand). 249 if len(filteredHashes) == 0 { 250 break 251 } 252 } else { 253 filteredHashes = txi.match(ctx, c, startKeyForCondition(c, height), filteredHashes, false) 254 } 255 } 256 257 results := make([]*types.TxResult, 0, len(filteredHashes)) 258 for _, h := range filteredHashes { 259 res, err := txi.Get(h) 260 if err != nil { 261 return nil, errors.Wrapf(err, "failed to get Tx{%X}", h) 262 } 263 results = append(results, res) 264 265 // Potentially exit early. 266 select { 267 case <-ctx.Done(): 268 break 269 default: 270 } 271 } 272 273 return results, nil 274 } 275 276 func lookForHash(conditions []query.Condition) (hash []byte, ok bool, err error) { 277 for _, c := range conditions { 278 if c.CompositeKey == types.TxHashKey { 279 decoded, err := hex.DecodeString(c.Operand.(string)) 280 return decoded, true, err 281 } 282 } 283 return 284 } 285 286 // lookForHeight returns a height if there is an "height=X" condition. 287 func lookForHeight(conditions []query.Condition) (height int64) { 288 for _, c := range conditions { 289 if c.CompositeKey == types.TxHeightKey && c.Op == query.OpEqual { 290 return c.Operand.(int64) 291 } 292 } 293 return 0 294 } 295 296 // special map to hold range conditions 297 // Example: account.number => queryRange{lowerBound: 1, upperBound: 5} 298 type queryRanges map[string]queryRange 299 300 type queryRange struct { 301 lowerBound interface{} // int || time.Time 302 upperBound interface{} // int || time.Time 303 key string 304 includeLowerBound bool 305 includeUpperBound bool 306 } 307 308 func (r queryRange) lowerBoundValue() interface{} { 309 if r.lowerBound == nil { 310 return nil 311 } 312 313 if r.includeLowerBound { 314 return r.lowerBound 315 } 316 317 switch t := r.lowerBound.(type) { 318 case int64: 319 return t + 1 320 case time.Time: 321 return t.Unix() + 1 322 default: 323 panic("not implemented") 324 } 325 } 326 327 func (r queryRange) AnyBound() interface{} { 328 if r.lowerBound != nil { 329 return r.lowerBound 330 } 331 332 return r.upperBound 333 } 334 335 func (r queryRange) upperBoundValue() interface{} { 336 if r.upperBound == nil { 337 return nil 338 } 339 340 if r.includeUpperBound { 341 return r.upperBound 342 } 343 344 switch t := r.upperBound.(type) { 345 case int64: 346 return t - 1 347 case time.Time: 348 return t.Unix() - 1 349 default: 350 panic("not implemented") 351 } 352 } 353 354 func lookForRanges(conditions []query.Condition) (ranges queryRanges, indexes []int) { 355 ranges = make(queryRanges) 356 for i, c := range conditions { 357 if isRangeOperation(c.Op) { 358 r, ok := ranges[c.CompositeKey] 359 if !ok { 360 r = queryRange{key: c.CompositeKey} 361 } 362 switch c.Op { 363 case query.OpGreater: 364 r.lowerBound = c.Operand 365 case query.OpGreaterEqual: 366 r.includeLowerBound = true 367 r.lowerBound = c.Operand 368 case query.OpLess: 369 r.upperBound = c.Operand 370 case query.OpLessEqual: 371 r.includeUpperBound = true 372 r.upperBound = c.Operand 373 } 374 ranges[c.CompositeKey] = r 375 indexes = append(indexes, i) 376 } 377 } 378 return ranges, indexes 379 } 380 381 func isRangeOperation(op query.Operator) bool { 382 switch op { 383 case query.OpGreater, query.OpGreaterEqual, query.OpLess, query.OpLessEqual: 384 return true 385 default: 386 return false 387 } 388 } 389 390 // match returns all matching txs by hash that meet a given condition and start 391 // key. An already filtered result (filteredHashes) is provided such that any 392 // non-intersecting matches are removed. 393 // 394 // NOTE: filteredHashes may be empty if no previous condition has matched. 395 func (txi *TxIndex) match( 396 ctx context.Context, 397 c query.Condition, 398 startKeyBz []byte, 399 filteredHashes map[string][]byte, 400 firstRun bool, 401 ) map[string][]byte { 402 // A previous match was attempted but resulted in no matches, so we return 403 // no matches (assuming AND operand). 404 if !firstRun && len(filteredHashes) == 0 { 405 return filteredHashes 406 } 407 408 tmpHashes := make(map[string][]byte) 409 410 switch { 411 case c.Op == query.OpEqual: 412 it, err := dbm.IteratePrefix(txi.store, startKeyBz) 413 if err != nil { 414 panic(err) 415 } 416 defer it.Close() 417 418 for ; it.Valid(); it.Next() { 419 tmpHashes[string(it.Value())] = it.Value() 420 421 // Potentially exit early. 422 select { 423 case <-ctx.Done(): 424 break 425 default: 426 } 427 } 428 429 case c.Op == query.OpContains: 430 // XXX: startKey does not apply here. 431 // For example, if startKey = "account.owner/an/" and search query = "account.owner CONTAINS an" 432 // we can't iterate with prefix "account.owner/an/" because we might miss keys like "account.owner/Ulan/" 433 it, err := dbm.IteratePrefix(txi.store, startKey(c.CompositeKey)) 434 if err != nil { 435 panic(err) 436 } 437 defer it.Close() 438 439 for ; it.Valid(); it.Next() { 440 if !isTagKey(it.Key()) { 441 continue 442 } 443 444 if strings.Contains(extractValueFromKey(it.Key()), c.Operand.(string)) { 445 tmpHashes[string(it.Value())] = it.Value() 446 } 447 448 // Potentially exit early. 449 select { 450 case <-ctx.Done(): 451 break 452 default: 453 } 454 } 455 default: 456 panic("other operators should be handled already") 457 } 458 459 if len(tmpHashes) == 0 || firstRun { 460 // Either: 461 // 462 // 1. Regardless if a previous match was attempted, which may have had 463 // results, but no match was found for the current condition, then we 464 // return no matches (assuming AND operand). 465 // 466 // 2. A previous match was not attempted, so we return all results. 467 return tmpHashes 468 } 469 470 // Remove/reduce matches in filteredHashes that were not found in this 471 // match (tmpHashes). 472 for k := range filteredHashes { 473 if tmpHashes[k] == nil { 474 delete(filteredHashes, k) 475 476 // Potentially exit early. 477 select { 478 case <-ctx.Done(): 479 break 480 default: 481 } 482 } 483 } 484 485 return filteredHashes 486 } 487 488 // matchRange returns all matching txs by hash that meet a given queryRange and 489 // start key. An already filtered result (filteredHashes) is provided such that 490 // any non-intersecting matches are removed. 491 // 492 // NOTE: filteredHashes may be empty if no previous condition has matched. 493 func (txi *TxIndex) matchRange( 494 ctx context.Context, 495 r queryRange, 496 startKey []byte, 497 filteredHashes map[string][]byte, 498 firstRun bool, 499 ) map[string][]byte { 500 // A previous match was attempted but resulted in no matches, so we return 501 // no matches (assuming AND operand). 502 if !firstRun && len(filteredHashes) == 0 { 503 return filteredHashes 504 } 505 506 tmpHashes := make(map[string][]byte) 507 lowerBound := r.lowerBoundValue() 508 upperBound := r.upperBoundValue() 509 510 it, err := dbm.IteratePrefix(txi.store, startKey) 511 if err != nil { 512 panic(err) 513 } 514 defer it.Close() 515 516 LOOP: 517 for ; it.Valid(); it.Next() { 518 if !isTagKey(it.Key()) { 519 continue 520 } 521 522 if _, ok := r.AnyBound().(int64); ok { 523 v, err := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) 524 if err != nil { 525 continue LOOP 526 } 527 528 include := true 529 if lowerBound != nil && v < lowerBound.(int64) { 530 include = false 531 } 532 533 if upperBound != nil && v > upperBound.(int64) { 534 include = false 535 } 536 537 if include { 538 tmpHashes[string(it.Value())] = it.Value() 539 } 540 541 // XXX: passing time in a ABCI Events is not yet implemented 542 // case time.Time: 543 // v := strconv.ParseInt(extractValueFromKey(it.Key()), 10, 64) 544 // if v == r.upperBound { 545 // break 546 // } 547 } 548 549 // Potentially exit early. 550 select { 551 case <-ctx.Done(): 552 break 553 default: 554 } 555 } 556 557 if len(tmpHashes) == 0 || firstRun { 558 // Either: 559 // 560 // 1. Regardless if a previous match was attempted, which may have had 561 // results, but no match was found for the current condition, then we 562 // return no matches (assuming AND operand). 563 // 564 // 2. A previous match was not attempted, so we return all results. 565 return tmpHashes 566 } 567 568 // Remove/reduce matches in filteredHashes that were not found in this 569 // match (tmpHashes). 570 for k := range filteredHashes { 571 if tmpHashes[k] == nil { 572 delete(filteredHashes, k) 573 574 // Potentially exit early. 575 select { 576 case <-ctx.Done(): 577 break 578 default: 579 } 580 } 581 } 582 583 return filteredHashes 584 } 585 586 /////////////////////////////////////////////////////////////////////////////// 587 // Keys 588 589 func isTagKey(key []byte) bool { 590 return strings.Count(string(key), tagKeySeparator) == 3 591 } 592 593 func extractValueFromKey(key []byte) string { 594 parts := strings.SplitN(string(key), tagKeySeparator, 3) 595 return parts[1] 596 } 597 598 func keyForEvent(key string, value []byte, result *types.TxResult) []byte { 599 return []byte(fmt.Sprintf("%s/%s/%d/%d", 600 key, 601 value, 602 result.Height, 603 result.Index, 604 )) 605 } 606 607 func keyForHeight(result *types.TxResult) []byte { 608 return []byte(fmt.Sprintf("%s/%d/%d/%d", 609 types.TxHeightKey, 610 result.Height, 611 result.Height, 612 result.Index, 613 )) 614 } 615 616 func startKeyForCondition(c query.Condition, height int64) []byte { 617 if height > 0 { 618 return startKey(c.CompositeKey, c.Operand, height) 619 } 620 return startKey(c.CompositeKey, c.Operand) 621 } 622 623 func startKey(fields ...interface{}) []byte { 624 var b bytes.Buffer 625 for _, f := range fields { 626 b.Write([]byte(fmt.Sprintf("%v", f) + tagKeySeparator)) 627 } 628 return b.Bytes() 629 }