github.com/decred/dcrlnd@v0.7.6/watchtower/wtdb/tower_db.go (about) 1 package wtdb 2 3 import ( 4 "bytes" 5 "errors" 6 7 "github.com/decred/dcrd/chaincfg/chainhash" 8 "github.com/decred/dcrlnd/chainntnfs" 9 "github.com/decred/dcrlnd/kvdb" 10 "github.com/decred/dcrlnd/watchtower/blob" 11 ) 12 13 var ( 14 // sessionsBkt is a bucket containing all negotiated client sessions. 15 // session id -> session 16 sessionsBkt = []byte("sessions-bucket") 17 18 // updatesBkt is a bucket containing all state updates sent by clients. 19 // The updates are further bucketed by session id to prevent clients 20 // from overwrite each other. 21 // hint => session id -> update 22 updatesBkt = []byte("updates-bucket") 23 24 // updateIndexBkt is a bucket that indexes all state updates by their 25 // overarching session id. This allows for efficient lookup of updates 26 // by their session id, which is currently used to aide deletion 27 // performance. 28 // session id => hint1 -> []byte{} 29 // => hint2 -> []byte{} 30 updateIndexBkt = []byte("update-index-bucket") 31 32 // lookoutTipBkt is a bucket containing the last block epoch processed 33 // by the lookout subsystem. It has one key, lookoutTipKey. 34 // lookoutTipKey -> block epoch 35 lookoutTipBkt = []byte("lookout-tip-bucket") 36 37 // lookoutTipKey is a static key used to retrieve lookout tip's block 38 // epoch from the lookoutTipBkt. 39 lookoutTipKey = []byte("lookout-tip") 40 41 // ErrNoSessionHintIndex signals that an active session does not have an 42 // initialized index for tracking its own state updates. 43 ErrNoSessionHintIndex = errors.New("session hint index missing") 44 45 // ErrInvalidBlobSize indicates that the encrypted blob provided by the 46 // client is not valid according to the blob type of the session. 47 ErrInvalidBlobSize = errors.New("invalid blob size") 48 ) 49 50 // TowerDB is single database providing a persistent storage engine for the 51 // wtserver and lookout subsystems. 52 type TowerDB struct { 53 db kvdb.Backend 54 } 55 56 // OpenTowerDB opens the tower database given the path to the database's 57 // directory. If no such database exists, this method will initialize a fresh 58 // one using the latest version number and bucket structure. If a database 59 // exists but has a lower version number than the current version, any necessary 60 // migrations will be applied before returning. Any attempt to open a database 61 // with a version number higher that the latest version will fail to prevent 62 // accidental reversion. 63 func OpenTowerDB(db kvdb.Backend) (*TowerDB, error) { 64 firstInit, err := isFirstInit(db) 65 if err != nil { 66 return nil, err 67 } 68 69 towerDB := &TowerDB{ 70 db: db, 71 } 72 73 err = initOrSyncVersions(towerDB, firstInit, towerDBVersions) 74 if err != nil { 75 db.Close() 76 return nil, err 77 } 78 79 // Now that the database version fully consistent with our latest known 80 // version, ensure that all top-level buckets known to this version are 81 // initialized. This allows us to assume their presence throughout all 82 // operations. If an known top-level bucket is expected to exist but is 83 // missing, this will trigger a ErrUninitializedDB error. 84 err = kvdb.Update(towerDB.db, initTowerDBBuckets, func() {}) 85 if err != nil { 86 db.Close() 87 return nil, err 88 } 89 90 return towerDB, nil 91 } 92 93 // initTowerDBBuckets creates all top-level buckets required to handle database 94 // operations required by the latest version. 95 func initTowerDBBuckets(tx kvdb.RwTx) error { 96 buckets := [][]byte{ 97 sessionsBkt, 98 updateIndexBkt, 99 updatesBkt, 100 lookoutTipBkt, 101 } 102 103 for _, bucket := range buckets { 104 _, err := tx.CreateTopLevelBucket(bucket) 105 if err != nil { 106 return err 107 } 108 } 109 110 return nil 111 } 112 113 // bdb returns the backing bolt.DB instance. 114 // 115 // NOTE: Part of the versionedDB interface. 116 func (t *TowerDB) bdb() kvdb.Backend { 117 return t.db 118 } 119 120 // Version returns the database's current version number. 121 // 122 // NOTE: Part of the versionedDB interface. 123 func (t *TowerDB) Version() (uint32, error) { 124 var version uint32 125 err := kvdb.View(t.db, func(tx kvdb.RTx) error { 126 var err error 127 version, err = getDBVersion(tx) 128 return err 129 }, func() { 130 version = 0 131 }) 132 if err != nil { 133 return 0, err 134 } 135 136 return version, nil 137 } 138 139 // Close closes the underlying database. 140 func (t *TowerDB) Close() error { 141 return t.db.Close() 142 } 143 144 // GetSessionInfo retrieves the session for the passed session id. An error is 145 // returned if the session could not be found. 146 func (t *TowerDB) GetSessionInfo(id *SessionID) (*SessionInfo, error) { 147 var session *SessionInfo 148 err := kvdb.View(t.db, func(tx kvdb.RTx) error { 149 sessions := tx.ReadBucket(sessionsBkt) 150 if sessions == nil { 151 return ErrUninitializedDB 152 } 153 154 var err error 155 session, err = getSession(sessions, id[:]) 156 return err 157 }, func() { 158 session = nil 159 }) 160 if err != nil { 161 return nil, err 162 } 163 164 return session, nil 165 } 166 167 // InsertSessionInfo records a negotiated session in the tower database. An 168 // error is returned if the session already exists. 169 func (t *TowerDB) InsertSessionInfo(session *SessionInfo) error { 170 return kvdb.Update(t.db, func(tx kvdb.RwTx) error { 171 sessions := tx.ReadWriteBucket(sessionsBkt) 172 if sessions == nil { 173 return ErrUninitializedDB 174 } 175 176 updateIndex := tx.ReadWriteBucket(updateIndexBkt) 177 if updateIndex == nil { 178 return ErrUninitializedDB 179 } 180 181 dbSession, err := getSession(sessions, session.ID[:]) 182 switch { 183 case err == ErrSessionNotFound: 184 // proceed. 185 186 case err != nil: 187 return err 188 189 case dbSession.LastApplied > 0: 190 return ErrSessionAlreadyExists 191 } 192 193 // Perform a quick sanity check on the session policy before 194 // accepting. 195 if err := session.Policy.Validate(); err != nil { 196 return err 197 } 198 199 err = putSession(sessions, session) 200 if err != nil { 201 return err 202 } 203 204 // Initialize the session-hint index which will be used to track 205 // all updates added for this session. Upon deletion, we will 206 // consult the index to determine exactly which updates should 207 // be deleted without needing to iterate over the entire 208 // database. 209 return touchSessionHintBkt(updateIndex, &session.ID) 210 }, func() {}) 211 } 212 213 // InsertStateUpdate stores an update sent by the client after validating that 214 // the update is well-formed in the context of other updates sent for the same 215 // session. This include verifying that the sequence number is incremented 216 // properly and the last applied values echoed by the client are sane. 217 func (t *TowerDB) InsertStateUpdate(update *SessionStateUpdate) (uint16, error) { 218 var lastApplied uint16 219 err := kvdb.Update(t.db, func(tx kvdb.RwTx) error { 220 sessions := tx.ReadWriteBucket(sessionsBkt) 221 if sessions == nil { 222 return ErrUninitializedDB 223 } 224 225 updates := tx.ReadWriteBucket(updatesBkt) 226 if updates == nil { 227 return ErrUninitializedDB 228 } 229 230 updateIndex := tx.ReadWriteBucket(updateIndexBkt) 231 if updateIndex == nil { 232 return ErrUninitializedDB 233 } 234 235 // Fetch the session corresponding to the update's session id. 236 // This will be used to validate that the update's sequence 237 // number and last applied values are sane. 238 session, err := getSession(sessions, update.ID[:]) 239 if err != nil { 240 return err 241 } 242 243 // Assert that the blob is the correct size for the session's 244 // blob type. 245 expBlobSize := blob.Size(session.Policy.BlobType) 246 if len(update.EncryptedBlob) != expBlobSize { 247 return ErrInvalidBlobSize 248 } 249 250 // Validate the update against the current state of the session. 251 err = session.AcceptUpdateSequence( 252 update.SeqNum, update.LastApplied, 253 ) 254 if err != nil { 255 return err 256 } 257 258 // Validation succeeded, therefore the update is committed and 259 // the session's last applied value is equal to the update's 260 // sequence number. 261 lastApplied = session.LastApplied 262 263 // Store the updated session to persist the updated last applied 264 // values. 265 err = putSession(sessions, session) 266 if err != nil { 267 return err 268 } 269 270 // Create or load the hint bucket for this state update's hint 271 // and write the given update. 272 hints, err := updates.CreateBucketIfNotExists(update.Hint[:]) 273 if err != nil { 274 return err 275 } 276 277 var b bytes.Buffer 278 err = update.Encode(&b) 279 if err != nil { 280 return err 281 } 282 283 err = hints.Put(update.ID[:], b.Bytes()) 284 if err != nil { 285 return err 286 } 287 288 // Finally, create an entry in the update index to track this 289 // hint under its session id. This will allow us to delete the 290 // entries efficiently if the session is ever removed. 291 return putHintForSession(updateIndex, &update.ID, update.Hint) 292 }, func() { 293 lastApplied = 0 294 }) 295 if err != nil { 296 return 0, err 297 } 298 299 return lastApplied, nil 300 } 301 302 // DeleteSession removes all data associated with a particular session id from 303 // the tower's database. 304 func (t *TowerDB) DeleteSession(target SessionID) error { 305 return kvdb.Update(t.db, func(tx kvdb.RwTx) error { 306 sessions := tx.ReadWriteBucket(sessionsBkt) 307 if sessions == nil { 308 return ErrUninitializedDB 309 } 310 311 updates := tx.ReadWriteBucket(updatesBkt) 312 if updates == nil { 313 return ErrUninitializedDB 314 } 315 316 updateIndex := tx.ReadWriteBucket(updateIndexBkt) 317 if updateIndex == nil { 318 return ErrUninitializedDB 319 } 320 321 // Fail if the session doesn't exit. 322 _, err := getSession(sessions, target[:]) 323 if err != nil { 324 return err 325 } 326 327 // Remove the target session. 328 err = sessions.Delete(target[:]) 329 if err != nil { 330 return err 331 } 332 333 // Next, check the update index for any hints that were added 334 // under this session. 335 hints, err := getHintsForSession(updateIndex, &target) 336 if err != nil { 337 return err 338 } 339 340 for _, hint := range hints { 341 // Remove the state updates for any blobs stored under 342 // the target session identifier. 343 updatesForHint := updates.NestedReadWriteBucket(hint[:]) 344 if updatesForHint == nil { 345 continue 346 } 347 348 update := updatesForHint.Get(target[:]) 349 if update == nil { 350 continue 351 } 352 353 err := updatesForHint.Delete(target[:]) 354 if err != nil { 355 return err 356 } 357 358 // If this was the last state update, we can also remove 359 // the hint that would map to an empty set. 360 err = isBucketEmpty(updatesForHint) 361 switch { 362 363 // Other updates exist for this hint, keep the bucket. 364 case err == errBucketNotEmpty: 365 continue 366 367 // Unexpected error. 368 case err != nil: 369 return err 370 371 // No more updates for this hint, prune hint bucket. 372 default: 373 err = updates.DeleteNestedBucket(hint[:]) 374 if err != nil { 375 return err 376 } 377 } 378 } 379 380 // Finally, remove this session from the update index, which 381 // also removes any of the indexed hints beneath it. 382 return removeSessionHintBkt(updateIndex, &target) 383 }, func() {}) 384 } 385 386 // QueryMatches searches against all known state updates for any that match the 387 // passed breachHints. More than one Match will be returned for a given hint if 388 // they exist in the database. 389 func (t *TowerDB) QueryMatches(breachHints []blob.BreachHint) ([]Match, error) { 390 var matches []Match 391 err := kvdb.View(t.db, func(tx kvdb.RTx) error { 392 sessions := tx.ReadBucket(sessionsBkt) 393 if sessions == nil { 394 return ErrUninitializedDB 395 } 396 397 updates := tx.ReadBucket(updatesBkt) 398 if updates == nil { 399 return ErrUninitializedDB 400 } 401 402 // Iterate through the target breach hints, appending any 403 // matching updates to the set of matches. 404 for _, hint := range breachHints { 405 // If a bucket does not exist for this hint, no matches 406 // are known. 407 updatesForHint := updates.NestedReadBucket(hint[:]) 408 if updatesForHint == nil { 409 continue 410 } 411 412 // Otherwise, iterate through all (session id, update) 413 // pairs, creating a Match for each. 414 err := updatesForHint.ForEach(func(k, v []byte) error { 415 // Load the session via the session id for this 416 // update. The session info contains further 417 // instructions for how to process the state 418 // update. 419 session, err := getSession(sessions, k) 420 switch { 421 case err == ErrSessionNotFound: 422 log.Warnf("Missing session=%x for "+ 423 "matched state update hint=%x", 424 k, hint) 425 return nil 426 427 case err != nil: 428 return err 429 } 430 431 // Decode the state update containing the 432 // encrypted blob. 433 update := &SessionStateUpdate{} 434 err = update.Decode(bytes.NewReader(v)) 435 if err != nil { 436 return err 437 } 438 439 var id SessionID 440 copy(id[:], k) 441 442 // Construct the final match using the found 443 // update and its session info. 444 match := Match{ 445 ID: id, 446 SeqNum: update.SeqNum, 447 Hint: hint, 448 EncryptedBlob: update.EncryptedBlob, 449 SessionInfo: session, 450 } 451 452 matches = append(matches, match) 453 454 return nil 455 }) 456 if err != nil { 457 return err 458 } 459 } 460 461 return nil 462 }, func() { 463 matches = nil 464 }) 465 if err != nil { 466 return nil, err 467 } 468 469 return matches, nil 470 } 471 472 // SetLookoutTip stores the provided epoch as the latest lookout tip epoch in 473 // the tower database. 474 func (t *TowerDB) SetLookoutTip(epoch *chainntnfs.BlockEpoch) error { 475 return kvdb.Update(t.db, func(tx kvdb.RwTx) error { 476 lookoutTip := tx.ReadWriteBucket(lookoutTipBkt) 477 if lookoutTip == nil { 478 return ErrUninitializedDB 479 } 480 481 return putLookoutEpoch(lookoutTip, epoch) 482 }, func() {}) 483 } 484 485 // GetLookoutTip retrieves the current lookout tip block epoch from the tower 486 // database. 487 func (t *TowerDB) GetLookoutTip() (*chainntnfs.BlockEpoch, error) { 488 var epoch *chainntnfs.BlockEpoch 489 err := kvdb.View(t.db, func(tx kvdb.RTx) error { 490 lookoutTip := tx.ReadBucket(lookoutTipBkt) 491 if lookoutTip == nil { 492 return ErrUninitializedDB 493 } 494 495 epoch = getLookoutEpoch(lookoutTip) 496 497 return nil 498 }, func() { 499 epoch = nil 500 }) 501 if err != nil { 502 return nil, err 503 } 504 505 return epoch, nil 506 } 507 508 // getSession retrieves the session info from the sessions bucket identified by 509 // its session id. An error is returned if the session is not found or a 510 // deserialization error occurs. 511 func getSession(sessions kvdb.RBucket, id []byte) (*SessionInfo, error) { 512 sessionBytes := sessions.Get(id) 513 if sessionBytes == nil { 514 return nil, ErrSessionNotFound 515 } 516 517 var session SessionInfo 518 err := session.Decode(bytes.NewReader(sessionBytes)) 519 if err != nil { 520 return nil, err 521 } 522 523 return &session, nil 524 } 525 526 // putSession stores the session info in the sessions bucket identified by its 527 // session id. An error is returned if a serialization error occurs. 528 func putSession(sessions kvdb.RwBucket, session *SessionInfo) error { 529 var b bytes.Buffer 530 err := session.Encode(&b) 531 if err != nil { 532 return err 533 } 534 535 return sessions.Put(session.ID[:], b.Bytes()) 536 } 537 538 // touchSessionHintBkt initializes the session-hint bucket for a particular 539 // session id. This ensures that future calls to getHintsForSession or 540 // putHintForSession can rely on the bucket already being created, and fail if 541 // index has not been initialized as this points to improper usage. 542 func touchSessionHintBkt(updateIndex kvdb.RwBucket, id *SessionID) error { 543 _, err := updateIndex.CreateBucketIfNotExists(id[:]) 544 return err 545 } 546 547 // removeSessionHintBkt prunes the session-hint bucket for the given session id 548 // and all of the hints contained inside. This should be used to clean up the 549 // index upon session deletion. 550 func removeSessionHintBkt(updateIndex kvdb.RwBucket, id *SessionID) error { 551 return updateIndex.DeleteNestedBucket(id[:]) 552 } 553 554 // getHintsForSession returns all known hints belonging to the given session id. 555 // If the index for the session has not been initialized, this method returns 556 // ErrNoSessionHintIndex. 557 func getHintsForSession(updateIndex kvdb.RBucket, 558 id *SessionID) ([]blob.BreachHint, error) { 559 560 sessionHints := updateIndex.NestedReadBucket(id[:]) 561 if sessionHints == nil { 562 return nil, ErrNoSessionHintIndex 563 } 564 565 var hints []blob.BreachHint 566 err := sessionHints.ForEach(func(k, _ []byte) error { 567 if len(k) != blob.BreachHintSize { 568 return nil 569 } 570 571 var hint blob.BreachHint 572 copy(hint[:], k) 573 hints = append(hints, hint) 574 return nil 575 }) 576 if err != nil { 577 return nil, err 578 } 579 580 return hints, nil 581 } 582 583 // putHintForSession inserts a record into the update index for a given 584 // (session, hint) pair. The hints are coalesced under a bucket for the target 585 // session id, and used to perform efficient removal of updates. If the index 586 // for the session has not been initialized, this method returns 587 // ErrNoSessionHintIndex. 588 func putHintForSession(updateIndex kvdb.RwBucket, id *SessionID, 589 hint blob.BreachHint) error { 590 591 sessionHints := updateIndex.NestedReadWriteBucket(id[:]) 592 if sessionHints == nil { 593 return ErrNoSessionHintIndex 594 } 595 596 return sessionHints.Put(hint[:], []byte{}) 597 } 598 599 // putLookoutEpoch stores the given lookout tip block epoch in provided bucket. 600 func putLookoutEpoch(bkt kvdb.RwBucket, epoch *chainntnfs.BlockEpoch) error { 601 epochBytes := make([]byte, 36) 602 copy(epochBytes, epoch.Hash[:]) 603 byteOrder.PutUint32(epochBytes[32:], uint32(epoch.Height)) 604 605 return bkt.Put(lookoutTipKey, epochBytes) 606 } 607 608 // getLookoutEpoch retrieves the lookout tip block epoch from the given bucket. 609 // A nil epoch is returned if no update exists. 610 func getLookoutEpoch(bkt kvdb.RBucket) *chainntnfs.BlockEpoch { 611 epochBytes := bkt.Get(lookoutTipKey) 612 if len(epochBytes) != 36 { 613 return nil 614 } 615 616 var hash chainhash.Hash 617 copy(hash[:], epochBytes[:32]) 618 height := byteOrder.Uint32(epochBytes[32:]) 619 620 return &chainntnfs.BlockEpoch{ 621 Hash: &hash, 622 Height: int32(height), 623 } 624 } 625 626 // errBucketNotEmpty is a helper error returned when testing whether a bucket is 627 // empty or not. 628 var errBucketNotEmpty = errors.New("bucket not empty") 629 630 // isBucketEmpty returns errBucketNotEmpty if the bucket is not empty. 631 func isBucketEmpty(bkt kvdb.RBucket) error { 632 return bkt.ForEach(func(_, _ []byte) error { 633 return errBucketNotEmpty 634 }) 635 }