github.com/decred/dcrlnd@v0.7.6/watchtower/wtdb/tower_db_test.go (about)

     1  package wtdb_test
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/binary"
     6  	"io/ioutil"
     7  	"os"
     8  	"reflect"
     9  	"testing"
    10  
    11  	"github.com/decred/dcrd/chaincfg/chainhash"
    12  	"github.com/decred/dcrlnd/chainntnfs"
    13  	"github.com/decred/dcrlnd/kvdb"
    14  	"github.com/decred/dcrlnd/watchtower"
    15  	"github.com/decred/dcrlnd/watchtower/blob"
    16  	"github.com/decred/dcrlnd/watchtower/wtdb"
    17  	"github.com/decred/dcrlnd/watchtower/wtmock"
    18  	"github.com/decred/dcrlnd/watchtower/wtpolicy"
    19  )
    20  
    21  var (
    22  	testBlob = make([]byte, blob.Size(blob.TypeAltruistCommit))
    23  )
    24  
    25  // dbInit is a closure used to initialize a watchtower.DB instance and its
    26  // cleanup function.
    27  type dbInit func(*testing.T) (watchtower.DB, func())
    28  
    29  // towerDBHarness holds the resources required to execute the tower db tests.
    30  type towerDBHarness struct {
    31  	t  *testing.T
    32  	db watchtower.DB
    33  }
    34  
    35  // newTowerDBHarness initializes a fresh test harness for testing watchtower.DB
    36  // implementations.
    37  func newTowerDBHarness(t *testing.T, init dbInit) (*towerDBHarness, func()) {
    38  	db, cleanup := init(t)
    39  
    40  	h := &towerDBHarness{
    41  		t:  t,
    42  		db: db,
    43  	}
    44  
    45  	return h, cleanup
    46  }
    47  
    48  // insertSession attempts to isnert the passed session and asserts that the
    49  // error returned matches expErr.
    50  func (h *towerDBHarness) insertSession(s *wtdb.SessionInfo, expErr error) {
    51  	h.t.Helper()
    52  
    53  	err := h.db.InsertSessionInfo(s)
    54  	if err != expErr {
    55  		h.t.Fatalf("expected insert session error: %v, got : %v",
    56  			expErr, err)
    57  	}
    58  }
    59  
    60  // getSession retrieves the session identified by id, asserting that the call
    61  // returns expErr. If successful, the found session is returned.
    62  func (h *towerDBHarness) getSession(id *wtdb.SessionID,
    63  	expErr error) *wtdb.SessionInfo {
    64  
    65  	h.t.Helper()
    66  
    67  	session, err := h.db.GetSessionInfo(id)
    68  	if err != expErr {
    69  		h.t.Fatalf("expected get session error: %v, got: %v",
    70  			expErr, err)
    71  	}
    72  
    73  	return session
    74  }
    75  
    76  // insertUpdate attempts to insert the passed state update and asserts that the
    77  // error returned matches expErr. If successful, the session's last applied
    78  // value is returned.
    79  func (h *towerDBHarness) insertUpdate(s *wtdb.SessionStateUpdate,
    80  	expErr error) uint16 {
    81  
    82  	h.t.Helper()
    83  
    84  	lastApplied, err := h.db.InsertStateUpdate(s)
    85  	if err != expErr {
    86  		h.t.Fatalf("expected insert update error: %v, got: %v",
    87  			expErr, err)
    88  	}
    89  
    90  	return lastApplied
    91  }
    92  
    93  // deleteSession attempts to delete the session identified by id and asserts
    94  // that the error returned from DeleteSession matches the expected error.
    95  func (h *towerDBHarness) deleteSession(id wtdb.SessionID, expErr error) {
    96  	h.t.Helper()
    97  
    98  	err := h.db.DeleteSession(id)
    99  	if err != expErr {
   100  		h.t.Fatalf("expected deletion error: %v, got: %v",
   101  			expErr, err)
   102  	}
   103  }
   104  
   105  // queryMatches queries that database for the passed breach hint, returning all
   106  // matches found.
   107  func (h *towerDBHarness) queryMatches(hint blob.BreachHint) []wtdb.Match {
   108  	h.t.Helper()
   109  
   110  	matches, err := h.db.QueryMatches([]blob.BreachHint{hint})
   111  	if err != nil {
   112  		h.t.Fatalf("unable to query matches: %v", err)
   113  	}
   114  
   115  	return matches
   116  }
   117  
   118  // hasUpdate queries the database for the passed breach hint, asserting that
   119  // only one match is present and that the hints indeed match. If successful, the
   120  // match is returned.
   121  func (h *towerDBHarness) hasUpdate(hint blob.BreachHint) wtdb.Match {
   122  	h.t.Helper()
   123  
   124  	matches := h.queryMatches(hint)
   125  	if len(matches) != 1 {
   126  		h.t.Fatalf("expected 1 match, found: %d", len(matches))
   127  	}
   128  
   129  	match := matches[0]
   130  	if match.Hint != hint {
   131  		h.t.Fatalf("expected hint: %x, got: %x", hint, match.Hint)
   132  	}
   133  
   134  	return match
   135  }
   136  
   137  // testInsertSession asserts that a session can only be inserted if a session
   138  // with the same session id does not already exist.
   139  func testInsertSession(h *towerDBHarness) {
   140  	var id wtdb.SessionID
   141  	h.getSession(&id, wtdb.ErrSessionNotFound)
   142  
   143  	session := &wtdb.SessionInfo{
   144  		ID: id,
   145  		Policy: wtpolicy.Policy{
   146  			TxPolicy: wtpolicy.TxPolicy{
   147  				BlobType: blob.TypeAltruistCommit,
   148  			},
   149  			MaxUpdates: 100,
   150  		},
   151  		RewardAddress: []byte{0x01, 0x02, 0x03},
   152  	}
   153  
   154  	// Try to insert the session, which should fail since the policy doesn't
   155  	// meet the current sanity checks.
   156  	h.insertSession(session, wtpolicy.ErrSweepFeeRateTooLow)
   157  
   158  	// Now assign a sane sweep fee rate to the policy, inserting should
   159  	// succeed.
   160  	session.Policy.SweepFeeRate = wtpolicy.DefaultSweepFeeRate
   161  	h.insertSession(session, nil)
   162  
   163  	session2 := h.getSession(&id, nil)
   164  
   165  	if !reflect.DeepEqual(session, session2) {
   166  		h.t.Fatalf("expected session: %v, got %v",
   167  			session, session2)
   168  	}
   169  
   170  	h.insertSession(session, nil)
   171  
   172  	// Insert a state update to fully commit the session parameters.
   173  	update := &wtdb.SessionStateUpdate{
   174  		ID:            id,
   175  		SeqNum:        1,
   176  		EncryptedBlob: testBlob,
   177  	}
   178  	h.insertUpdate(update, nil)
   179  
   180  	// Trying to insert a new session under the same ID should fail.
   181  	h.insertSession(session, wtdb.ErrSessionAlreadyExists)
   182  }
   183  
   184  // testMultipleMatches asserts that if multiple sessions insert state updates
   185  // with the same breach hint that all will be returned from QueryMatches.
   186  func testMultipleMatches(h *towerDBHarness) {
   187  	const numUpdates = 3
   188  
   189  	// Create a new session and send updates with all the same hint.
   190  	var hint blob.BreachHint
   191  	for i := 0; i < numUpdates; i++ {
   192  		id := *id(i)
   193  		session := &wtdb.SessionInfo{
   194  			ID: id,
   195  			Policy: wtpolicy.Policy{
   196  				TxPolicy: wtpolicy.TxPolicy{
   197  					BlobType:     blob.TypeAltruistCommit,
   198  					SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   199  				},
   200  				MaxUpdates: 3,
   201  			},
   202  			RewardAddress: []byte{},
   203  		}
   204  		h.insertSession(session, nil)
   205  
   206  		update := &wtdb.SessionStateUpdate{
   207  			ID:            id,
   208  			SeqNum:        1,
   209  			Hint:          hint, // Use same hint to cause multiple matches
   210  			EncryptedBlob: testBlob,
   211  		}
   212  		h.insertUpdate(update, nil)
   213  	}
   214  
   215  	// Query the db for matches on the chosen hint.
   216  	matches := h.queryMatches(hint)
   217  	if len(matches) != numUpdates {
   218  		h.t.Fatalf("num updates mismatch, want: %d, got: %d",
   219  			numUpdates, len(matches))
   220  	}
   221  
   222  	// Assert that the hints are what we asked for, and compute the set of
   223  	// sessions returned.
   224  	sessions := make(map[wtdb.SessionID]struct{})
   225  	for _, match := range matches {
   226  		if match.Hint != hint {
   227  			h.t.Fatalf("hint mismatch, want: %v, got: %v",
   228  				hint, match.Hint)
   229  		}
   230  		sessions[match.ID] = struct{}{}
   231  	}
   232  
   233  	// Assert that the sessions returned match the session ids of the
   234  	// sessions we initially created.
   235  	for i := 0; i < numUpdates; i++ {
   236  		if _, ok := sessions[*id(i)]; !ok {
   237  			h.t.Fatalf("match for session %v not found", *id(i))
   238  		}
   239  	}
   240  }
   241  
   242  // testLookoutTip asserts that the database properly stores and returns the
   243  // lookout tip block epochs. It also asserts that the epoch returned is nil when
   244  // no tip has ever been set.
   245  func testLookoutTip(h *towerDBHarness) {
   246  	// Retrieve lookout tip on fresh db.
   247  	epoch, err := h.db.GetLookoutTip()
   248  	if err != nil {
   249  		h.t.Fatalf("unable to fetch lookout tip: %v", err)
   250  	}
   251  
   252  	// Assert that the epoch is nil.
   253  	if epoch != nil {
   254  		h.t.Fatalf("lookout tip should not be set, found: %v", epoch)
   255  	}
   256  
   257  	// Create a closure that inserts an epoch, retrieves it, and asserts
   258  	// that the returned epoch matches what was inserted.
   259  	setAndCheck := func(i int) {
   260  		expEpoch := epochFromInt(1)
   261  		err = h.db.SetLookoutTip(expEpoch)
   262  		if err != nil {
   263  			h.t.Fatalf("unable to set lookout tip: %v", err)
   264  		}
   265  
   266  		epoch, err = h.db.GetLookoutTip()
   267  		if err != nil {
   268  			h.t.Fatalf("unable to fetch lookout tip: %v", err)
   269  		}
   270  
   271  		if !reflect.DeepEqual(epoch, expEpoch) {
   272  			h.t.Fatalf("lookout tip mismatch, want: %v, got: %v",
   273  				expEpoch, epoch)
   274  		}
   275  	}
   276  
   277  	// Set and assert the lookout tip.
   278  	for i := 0; i < 5; i++ {
   279  		setAndCheck(i)
   280  	}
   281  }
   282  
   283  // testDeleteSession asserts the behavior of a tower database when deleting
   284  // session data. The test asserts that the only proper the target session is
   285  // remmoved, and that only updates for a particular session are pruned.
   286  func testDeleteSession(h *towerDBHarness) {
   287  	// First, create a session so that the database is not empty.
   288  	id0 := id(0)
   289  	session0 := &wtdb.SessionInfo{
   290  		ID: *id0,
   291  		Policy: wtpolicy.Policy{
   292  			TxPolicy: wtpolicy.TxPolicy{
   293  				BlobType:     blob.TypeAltruistCommit,
   294  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   295  			},
   296  			MaxUpdates: 3,
   297  		},
   298  		RewardAddress: []byte{},
   299  	}
   300  	h.insertSession(session0, nil)
   301  
   302  	// Now, attempt to delete a session which does not exist, that is also
   303  	// different from the first one created.
   304  	id1 := id(1)
   305  	h.deleteSession(*id1, wtdb.ErrSessionNotFound)
   306  
   307  	// The first session should still be present.
   308  	h.getSession(id0, nil)
   309  
   310  	// Now insert a second session under a different id.
   311  	session1 := &wtdb.SessionInfo{
   312  		ID: *id1,
   313  		Policy: wtpolicy.Policy{
   314  			TxPolicy: wtpolicy.TxPolicy{
   315  				BlobType:     blob.TypeAltruistCommit,
   316  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   317  			},
   318  			MaxUpdates: 3,
   319  		},
   320  		RewardAddress: []byte{},
   321  	}
   322  	h.insertSession(session1, nil)
   323  
   324  	// Create and insert updates for both sessions that have the same hint.
   325  	var hint blob.BreachHint
   326  	update0 := &wtdb.SessionStateUpdate{
   327  		ID:            *id0,
   328  		Hint:          hint,
   329  		SeqNum:        1,
   330  		EncryptedBlob: testBlob,
   331  	}
   332  	update1 := &wtdb.SessionStateUpdate{
   333  		ID:            *id1,
   334  		Hint:          hint,
   335  		SeqNum:        1,
   336  		EncryptedBlob: testBlob,
   337  	}
   338  
   339  	// Insert both updates should succeed.
   340  	h.insertUpdate(update0, nil)
   341  	h.insertUpdate(update1, nil)
   342  
   343  	// Remove the new session, which should succeed.
   344  	h.deleteSession(*id1, nil)
   345  
   346  	// The first session should still be present.
   347  	h.getSession(id0, nil)
   348  
   349  	// The second session should be removed.
   350  	h.getSession(id1, wtdb.ErrSessionNotFound)
   351  
   352  	// Assert that only one update is still present.
   353  	matches := h.queryMatches(hint)
   354  	if len(matches) != 1 {
   355  		h.t.Fatalf("expected one update, found: %d", len(matches))
   356  	}
   357  
   358  	// Assert that the update belongs to the first session.
   359  	if matches[0].ID != *id0 {
   360  		h.t.Fatalf("expected match for %v, instead is for: %v",
   361  			*id0, matches[0].ID)
   362  	}
   363  
   364  	// Finally, remove the first session added.
   365  	h.deleteSession(*id0, nil)
   366  
   367  	// The session should no longer be present.
   368  	h.getSession(id0, wtdb.ErrSessionNotFound)
   369  
   370  	// No matches should exist for this hint.
   371  	matches = h.queryMatches(hint)
   372  	if len(matches) != 0 {
   373  		h.t.Fatalf("expected zero updates, found: %d", len(matches))
   374  	}
   375  }
   376  
   377  type stateUpdateTest struct {
   378  	session    *wtdb.SessionInfo
   379  	sessionErr error
   380  	updates    []*wtdb.SessionStateUpdate
   381  	updateErrs []error
   382  }
   383  
   384  func runStateUpdateTest(test stateUpdateTest) func(*towerDBHarness) {
   385  	return func(h *towerDBHarness) {
   386  		// We may need to modify the initial session as we process
   387  		// updates to discern the expected state of the session. We'll
   388  		// create a copy of the test session if necessary to prevent
   389  		// mutations from impacting other tests.
   390  		var expSession *wtdb.SessionInfo
   391  
   392  		// Create the session if the tests requests one.
   393  		if test.session != nil {
   394  			// Copy the initial session and insert it into the
   395  			// database.
   396  			ogSession := *test.session
   397  			expErr := test.sessionErr
   398  			h.insertSession(&ogSession, expErr)
   399  
   400  			if expErr != nil {
   401  				return
   402  			}
   403  
   404  			// Copy the initial state of the accepted session.
   405  			expSession = &wtdb.SessionInfo{}
   406  			*expSession = *test.session
   407  		}
   408  
   409  		if len(test.updates) != len(test.updateErrs) {
   410  			h.t.Fatalf("malformed test case, num updates " +
   411  				"should match num errors")
   412  		}
   413  
   414  		// Send any updates provided in the test.
   415  		for i, update := range test.updates {
   416  			expErr := test.updateErrs[i]
   417  			h.insertUpdate(update, expErr)
   418  
   419  			if expErr != nil {
   420  				continue
   421  			}
   422  
   423  			// Don't perform the following checks and modfications
   424  			// if we don't have an expected session to compare
   425  			// against.
   426  			if expSession == nil {
   427  				continue
   428  			}
   429  
   430  			// Update the session's last applied and client last
   431  			// applied.
   432  			expSession.LastApplied = update.SeqNum
   433  			expSession.ClientLastApplied = update.LastApplied
   434  
   435  			match := h.hasUpdate(update.Hint)
   436  			if !reflect.DeepEqual(match.SessionInfo, expSession) {
   437  				h.t.Fatalf("expected session: %v, got: %v",
   438  					expSession, match.SessionInfo)
   439  			}
   440  		}
   441  	}
   442  }
   443  
   444  var stateUpdateNoSession = stateUpdateTest{
   445  	session: nil,
   446  	updates: []*wtdb.SessionStateUpdate{
   447  		updateFromInt(id(0), 1, 0),
   448  	},
   449  	updateErrs: []error{
   450  		wtdb.ErrSessionNotFound,
   451  	},
   452  }
   453  
   454  var stateUpdateExhaustSession = stateUpdateTest{
   455  	session: &wtdb.SessionInfo{
   456  		ID: *id(0),
   457  		Policy: wtpolicy.Policy{
   458  			TxPolicy: wtpolicy.TxPolicy{
   459  				BlobType:     blob.TypeAltruistCommit,
   460  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   461  			},
   462  			MaxUpdates: 3,
   463  		},
   464  		RewardAddress: []byte{},
   465  	},
   466  	updates: []*wtdb.SessionStateUpdate{
   467  		updateFromInt(id(0), 1, 0),
   468  		updateFromInt(id(0), 2, 0),
   469  		updateFromInt(id(0), 3, 0),
   470  		updateFromInt(id(0), 4, 0),
   471  	},
   472  	updateErrs: []error{
   473  		nil, nil, nil, wtdb.ErrSessionConsumed,
   474  	},
   475  }
   476  
   477  var stateUpdateSeqNumEqualLastApplied = stateUpdateTest{
   478  	session: &wtdb.SessionInfo{
   479  		ID: *id(0),
   480  		Policy: wtpolicy.Policy{
   481  			TxPolicy: wtpolicy.TxPolicy{
   482  				BlobType:     blob.TypeAltruistCommit,
   483  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   484  			},
   485  			MaxUpdates: 3,
   486  		},
   487  		RewardAddress: []byte{},
   488  	},
   489  	updates: []*wtdb.SessionStateUpdate{
   490  		updateFromInt(id(0), 1, 0),
   491  		updateFromInt(id(0), 2, 1),
   492  		updateFromInt(id(0), 3, 2),
   493  		updateFromInt(id(0), 3, 3),
   494  	},
   495  	updateErrs: []error{
   496  		nil, nil, nil, wtdb.ErrSeqNumAlreadyApplied,
   497  	},
   498  }
   499  
   500  var stateUpdateSeqNumLTLastApplied = stateUpdateTest{
   501  	session: &wtdb.SessionInfo{
   502  		ID: *id(0),
   503  		Policy: wtpolicy.Policy{
   504  			TxPolicy: wtpolicy.TxPolicy{
   505  				BlobType:     blob.TypeAltruistCommit,
   506  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   507  			},
   508  			MaxUpdates: 3,
   509  		},
   510  		RewardAddress: []byte{},
   511  	},
   512  	updates: []*wtdb.SessionStateUpdate{
   513  		updateFromInt(id(0), 1, 0),
   514  		updateFromInt(id(0), 2, 1),
   515  		updateFromInt(id(0), 1, 2),
   516  	},
   517  	updateErrs: []error{
   518  		nil, nil, wtdb.ErrSeqNumAlreadyApplied,
   519  	},
   520  }
   521  
   522  var stateUpdateSeqNumZeroInvalid = stateUpdateTest{
   523  	session: &wtdb.SessionInfo{
   524  		ID: *id(0),
   525  		Policy: wtpolicy.Policy{
   526  			TxPolicy: wtpolicy.TxPolicy{
   527  				BlobType:     blob.TypeAltruistCommit,
   528  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   529  			},
   530  			MaxUpdates: 3,
   531  		},
   532  		RewardAddress: []byte{},
   533  	},
   534  	updates: []*wtdb.SessionStateUpdate{
   535  		updateFromInt(id(0), 0, 0),
   536  	},
   537  	updateErrs: []error{
   538  		wtdb.ErrSeqNumAlreadyApplied,
   539  	},
   540  }
   541  
   542  var stateUpdateSkipSeqNum = stateUpdateTest{
   543  	session: &wtdb.SessionInfo{
   544  		ID: *id(0),
   545  		Policy: wtpolicy.Policy{
   546  			TxPolicy: wtpolicy.TxPolicy{
   547  				BlobType:     blob.TypeAltruistCommit,
   548  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   549  			},
   550  			MaxUpdates: 3,
   551  		},
   552  		RewardAddress: []byte{},
   553  	},
   554  	updates: []*wtdb.SessionStateUpdate{
   555  		updateFromInt(id(0), 2, 0),
   556  	},
   557  	updateErrs: []error{
   558  		wtdb.ErrUpdateOutOfOrder,
   559  	},
   560  }
   561  
   562  var stateUpdateRevertSeqNum = stateUpdateTest{
   563  	session: &wtdb.SessionInfo{
   564  		ID: *id(0),
   565  		Policy: wtpolicy.Policy{
   566  			TxPolicy: wtpolicy.TxPolicy{
   567  				BlobType:     blob.TypeAltruistCommit,
   568  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   569  			},
   570  			MaxUpdates: 3,
   571  		},
   572  		RewardAddress: []byte{},
   573  	},
   574  	updates: []*wtdb.SessionStateUpdate{
   575  		updateFromInt(id(0), 1, 0),
   576  		updateFromInt(id(0), 2, 0),
   577  		updateFromInt(id(0), 1, 0),
   578  	},
   579  	updateErrs: []error{
   580  		nil, nil, wtdb.ErrUpdateOutOfOrder,
   581  	},
   582  }
   583  
   584  var stateUpdateRevertLastApplied = stateUpdateTest{
   585  	session: &wtdb.SessionInfo{
   586  		ID: *id(0),
   587  		Policy: wtpolicy.Policy{
   588  			TxPolicy: wtpolicy.TxPolicy{
   589  				BlobType:     blob.TypeAltruistCommit,
   590  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   591  			},
   592  			MaxUpdates: 3,
   593  		},
   594  		RewardAddress: []byte{},
   595  	},
   596  	updates: []*wtdb.SessionStateUpdate{
   597  		updateFromInt(id(0), 1, 0),
   598  		updateFromInt(id(0), 2, 1),
   599  		updateFromInt(id(0), 3, 2),
   600  		updateFromInt(id(0), 4, 1),
   601  	},
   602  	updateErrs: []error{
   603  		nil, nil, nil, wtdb.ErrLastAppliedReversion,
   604  	},
   605  }
   606  
   607  var stateUpdateInvalidBlobSize = stateUpdateTest{
   608  	session: &wtdb.SessionInfo{
   609  		ID: *id(0),
   610  		Policy: wtpolicy.Policy{
   611  			TxPolicy: wtpolicy.TxPolicy{
   612  				BlobType:     blob.TypeAltruistCommit,
   613  				SweepFeeRate: wtpolicy.DefaultSweepFeeRate,
   614  			},
   615  			MaxUpdates: 3,
   616  		},
   617  		RewardAddress: []byte{},
   618  	},
   619  	updates: []*wtdb.SessionStateUpdate{
   620  		{
   621  			ID:            *id(0),
   622  			SeqNum:        1,
   623  			LastApplied:   0,
   624  			EncryptedBlob: []byte{0x01, 0x02, 0x03}, // too $hort
   625  		},
   626  	},
   627  	updateErrs: []error{
   628  		wtdb.ErrInvalidBlobSize,
   629  	},
   630  }
   631  
   632  func TestTowerDB(t *testing.T) {
   633  	dbCfg := &kvdb.BoltConfig{DBTimeout: kvdb.DefaultDBTimeout}
   634  	dbs := []struct {
   635  		name string
   636  		init dbInit
   637  	}{
   638  		{
   639  			name: "fresh bboltdb",
   640  			init: func(t *testing.T) (watchtower.DB, func()) {
   641  				path, err := ioutil.TempDir("", "towerdb")
   642  				if err != nil {
   643  					t.Fatalf("unable to make temp dir: %v",
   644  						err)
   645  				}
   646  
   647  				bdb, err := wtdb.NewBoltBackendCreator(
   648  					true, path, "watchtower.db",
   649  				)(dbCfg)
   650  				if err != nil {
   651  					os.RemoveAll(path)
   652  					t.Fatalf("unable to open db: %v", err)
   653  				}
   654  
   655  				db, err := wtdb.OpenTowerDB(bdb)
   656  				if err != nil {
   657  					os.RemoveAll(path)
   658  					t.Fatalf("unable to open db: %v", err)
   659  				}
   660  
   661  				cleanup := func() {
   662  					db.Close()
   663  					os.RemoveAll(path)
   664  				}
   665  
   666  				return db, cleanup
   667  			},
   668  		},
   669  		{
   670  			name: "reopened bboltdb",
   671  			init: func(t *testing.T) (watchtower.DB, func()) {
   672  				path, err := ioutil.TempDir("", "towerdb")
   673  				if err != nil {
   674  					t.Fatalf("unable to make temp dir: %v",
   675  						err)
   676  				}
   677  
   678  				bdb, err := wtdb.NewBoltBackendCreator(
   679  					true, path, "watchtower.db",
   680  				)(dbCfg)
   681  				if err != nil {
   682  					os.RemoveAll(path)
   683  					t.Fatalf("unable to open db: %v", err)
   684  				}
   685  
   686  				db, err := wtdb.OpenTowerDB(bdb)
   687  				if err != nil {
   688  					os.RemoveAll(path)
   689  					t.Fatalf("unable to open db: %v", err)
   690  				}
   691  				db.Close()
   692  
   693  				// Open the db again, ensuring we test a
   694  				// different path during open and that all
   695  				// buckets remain initialized.
   696  				bdb, err = wtdb.NewBoltBackendCreator(
   697  					true, path, "watchtower.db",
   698  				)(dbCfg)
   699  				if err != nil {
   700  					os.RemoveAll(path)
   701  					t.Fatalf("unable to open db: %v", err)
   702  				}
   703  
   704  				db, err = wtdb.OpenTowerDB(bdb)
   705  				if err != nil {
   706  					os.RemoveAll(path)
   707  					t.Fatalf("unable to open db: %v", err)
   708  				}
   709  
   710  				cleanup := func() {
   711  					db.Close()
   712  					os.RemoveAll(path)
   713  				}
   714  
   715  				return db, cleanup
   716  			},
   717  		},
   718  		{
   719  			name: "mock",
   720  			init: func(t *testing.T) (watchtower.DB, func()) {
   721  				return wtmock.NewTowerDB(), func() {}
   722  			},
   723  		},
   724  	}
   725  
   726  	tests := []struct {
   727  		name string
   728  		run  func(*towerDBHarness)
   729  	}{
   730  		{
   731  			name: "create session",
   732  			run:  testInsertSession,
   733  		},
   734  		{
   735  			name: "delete session",
   736  			run:  testDeleteSession,
   737  		},
   738  		{
   739  			name: "state update no session",
   740  			run:  runStateUpdateTest(stateUpdateNoSession),
   741  		},
   742  		{
   743  			name: "state update exhaust session",
   744  			run:  runStateUpdateTest(stateUpdateExhaustSession),
   745  		},
   746  		{
   747  			name: "state update seqnum equal last applied",
   748  			run: runStateUpdateTest(
   749  				stateUpdateSeqNumEqualLastApplied,
   750  			),
   751  		},
   752  		{
   753  			name: "state update seqnum less than last applied",
   754  			run: runStateUpdateTest(
   755  				stateUpdateSeqNumLTLastApplied,
   756  			),
   757  		},
   758  		{
   759  			name: "state update seqnum zero invalid",
   760  			run:  runStateUpdateTest(stateUpdateSeqNumZeroInvalid),
   761  		},
   762  		{
   763  			name: "state update skip seqnum",
   764  			run:  runStateUpdateTest(stateUpdateSkipSeqNum),
   765  		},
   766  		{
   767  			name: "state update revert seqnum",
   768  			run:  runStateUpdateTest(stateUpdateRevertSeqNum),
   769  		},
   770  		{
   771  			name: "state update revert last applied",
   772  			run:  runStateUpdateTest(stateUpdateRevertLastApplied),
   773  		},
   774  		{
   775  			name: "invalid blob size",
   776  			run:  runStateUpdateTest(stateUpdateInvalidBlobSize),
   777  		},
   778  		{
   779  			name: "multiple breach matches",
   780  			run:  testMultipleMatches,
   781  		},
   782  		{
   783  			name: "lookout tip",
   784  			run:  testLookoutTip,
   785  		},
   786  	}
   787  
   788  	for _, database := range dbs {
   789  		db := database
   790  		t.Run(db.name, func(t *testing.T) {
   791  			t.Parallel()
   792  
   793  			for _, test := range tests {
   794  				t.Run(test.name, func(t *testing.T) {
   795  					h, cleanup := newTowerDBHarness(
   796  						t, db.init,
   797  					)
   798  					defer cleanup()
   799  
   800  					test.run(h)
   801  				})
   802  			}
   803  		})
   804  	}
   805  }
   806  
   807  // id creates a session id from an integer.
   808  func id(i int) *wtdb.SessionID {
   809  	var id wtdb.SessionID
   810  	binary.BigEndian.PutUint32(id[:4], uint32(i))
   811  	return &id
   812  }
   813  
   814  // updateFromInt creates a unique update for a given (session, seqnum) pair. The
   815  // lastApplied argument can be used to construct updates simulating different
   816  // levels of synchronicity between client and db.
   817  func updateFromInt(id *wtdb.SessionID, i int,
   818  	lastApplied uint16) *wtdb.SessionStateUpdate {
   819  
   820  	// Ensure the hint is unique.
   821  	var hint blob.BreachHint
   822  	copy(hint[:4], id[:4])
   823  	binary.BigEndian.PutUint16(hint[4:6], uint16(i))
   824  
   825  	blobSize := blob.Size(blob.TypeAltruistCommit)
   826  
   827  	return &wtdb.SessionStateUpdate{
   828  		ID:            *id,
   829  		Hint:          hint,
   830  		SeqNum:        uint16(i),
   831  		LastApplied:   lastApplied,
   832  		EncryptedBlob: bytes.Repeat([]byte{byte(i)}, blobSize),
   833  	}
   834  }
   835  
   836  // epochFromInt creates a block epoch from an integer.
   837  func epochFromInt(i int) *chainntnfs.BlockEpoch {
   838  	var hash chainhash.Hash
   839  	binary.BigEndian.PutUint32(hash[:4], uint32(i))
   840  
   841  	return &chainntnfs.BlockEpoch{
   842  		Hash:   &hash,
   843  		Height: int32(i),
   844  	}
   845  }