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 }