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  }