From 5316fcd6c9c63e612d59e0187e4ccbc62cdfaea7 Mon Sep 17 00:00:00 2001 From: yyforyongyu Date: Sun, 1 May 2022 15:49:29 +0800 Subject: [PATCH] migration30+migtest: add unit tests for migration --- channeldb/migration30/migration_test.go | 450 ++++++++++++++++++++++++ channeldb/migtest/migtest.go | 39 ++ 2 files changed, 489 insertions(+) create mode 100644 channeldb/migration30/migration_test.go diff --git a/channeldb/migration30/migration_test.go b/channeldb/migration30/migration_test.go new file mode 100644 index 000000000..5f58ffb00 --- /dev/null +++ b/channeldb/migration30/migration_test.go @@ -0,0 +1,450 @@ +package migration30 + +import ( + "bytes" + "fmt" + "testing" + + mig25 "github.com/lightningnetwork/lnd/channeldb/migration25" + mig26 "github.com/lightningnetwork/lnd/channeldb/migration26" + "github.com/lightningnetwork/lnd/channeldb/migtest" + "github.com/lightningnetwork/lnd/kvdb" + "github.com/stretchr/testify/require" +) + +type ( + beforeMigrationFunc func(db kvdb.Backend) error + afterMigrationFunc func(t *testing.T, db kvdb.Backend) +) + +// TestMigrateRevocationLog provide a comprehensive test for the revocation log +// migration. The revocation logs are stored inside a deeply nested bucket, and +// can be accessed via nodePub:chainHash:fundingOutpoint:revocationLogBucket. +// Based on each value in the chain, we'd end up in a different db state. This +// test alters nodePub, fundingOutpoint, and revocationLogBucket to test +// against possible db states, leaving the chainHash staying the same as it's +// less likely to be changed. In specific, we test based on whether we have one +// or two peers(nodePub). For each peer, we test whether we have one or two +// channels(fundingOutpoint). And for each channel, we test 5 cases based on +// the revocation migration states(see buildChannelCases). The total states +// grow quickly and the test may take longer than 5min. +func TestMigrateRevocationLog(t *testing.T) { + t.Parallel() + + testCases := make([]*testCase, 0) + + // Create two peers, each has two channels. + alice1, alice2 := createTwoChannels() + bob1, bob2 := createTwoChannels() + + // Sort the two peers to match the order saved in boltdb. + if bytes.Compare( + alice1.IdentityPub.SerializeCompressed(), + bob1.IdentityPub.SerializeCompressed(), + ) > 0 { + + alice1, bob1 = bob1, alice1 + alice2, bob2 = bob2, alice2 + } + + // Build test cases for two peers. Each peer is independent so we + // combine the test cases based on its current db state. This would + // create a total of 30x30=900 cases. + for _, p1 := range buildPeerCases(alice1, alice2, false) { + for _, p2 := range buildPeerCases(bob1, bob2, p1.unfinished) { + setups := make([]beforeMigrationFunc, 0) + setups = append(setups, p1.setups...) + setups = append(setups, p2.setups...) + + asserters := make([]afterMigrationFunc, 0) + asserters = append(asserters, p1.asserters...) + asserters = append(asserters, p2.asserters...) + + name := fmt.Sprintf("alice: %s, bob: %s", + p1.name, p2.name) + + tc := &testCase{ + name: name, + setups: setups, + asserters: asserters, + } + testCases = append(testCases, tc) + } + } + + fmt.Printf("Running %d test cases...\n", len(testCases)) + + for i, tc := range testCases { + tc := tc + + // Construct a test case name that can be easily traced. + name := fmt.Sprintf("case_%d", i) + fmt.Println(name, tc.name) + + success := t.Run(name, func(t *testing.T) { + // Log the test's actual name on failure. + t.Log("Test setup: ", tc.name) + + beforeMigration := func(db kvdb.Backend) error { + for _, setup := range tc.setups { + if err := setup(db); err != nil { + return err + } + } + return nil + } + + afterMigration := func(db kvdb.Backend) error { + for _, asserter := range tc.asserters { + asserter(t, db) + } + return nil + } + + migtest.ApplyMigrationWithDb( + t, + beforeMigration, + afterMigration, + MigrateRevocationLog, + ) + }) + if !success { + return + } + } +} + +// createTwoChannels creates two channels that have the same chainHash and +// IdentityPub, simulating having two channels under the same peer. +func createTwoChannels() (*mig26.OpenChannel, *mig26.OpenChannel) { + // Create two channels under the same peer. + c1 := createTestChannel(nil) + c2 := createTestChannel(c1.IdentityPub) + + // If c1 is greater than c2, boltdb will put c2 before c1. + if bytes.Compare( + c1.FundingOutpoint.Hash[:], + c2.FundingOutpoint.Hash[:], + ) > 0 { + + c1, c2 = c2, c1 + } + + return c1, c2 +} + +// channelTestCase defines a single test case given a particular channel state. +type channelTestCase struct { + name string + setup beforeMigrationFunc + asserter afterMigrationFunc + unfinished bool +} + +// buildChannelCases builds five channel test cases. These cases can be viewed +// as basic units that are used to build more complex test cases based on +// number of channels and peers. +func buildChannelCases(c *mig26.OpenChannel, + overwrite bool) []*channelTestCase { + + // assertNewLogs is a helper closure that checks the old bucket and the + // two new logs are saved. + assertNewLogs := func(t *testing.T, db kvdb.Backend) { + // Check that the old bucket is removed. + assertOldLogBucketDeleted(t, db, c) + + l := fetchNewLog(t, db, c, logHeight1) + assertRevocationLog(t, newLog1, l) + + l = fetchNewLog(t, db, c, logHeight2) + assertRevocationLog(t, newLog2, l) + } + + // case1 defines a case where we don't have a chanBucket. + case1 := &channelTestCase{ + name: "no channel", + setup: func(db kvdb.Backend) error { + return setupTestLogs(db, nil, nil, nil) + }, + // No need to assert anything. + asserter: func(t *testing.T, db kvdb.Backend) {}, + } + + // case2 defines a case when the chanBucket has no old revocation logs. + case2 := &channelTestCase{ + name: "empty old logs", + setup: func(db kvdb.Backend) error { + return setupTestLogs(db, c, nil, nil) + }, + // No need to assert anything. + asserter: func(t *testing.T, db kvdb.Backend) {}, + } + + // case3 defines a case when the chanBucket has finished its migration. + case3 := &channelTestCase{ + name: "finished migration", + setup: func(db kvdb.Backend) error { + return createFinished(db, c) + }, + asserter: func(t *testing.T, db kvdb.Backend) { + // Check that the old bucket is removed. + assertOldLogBucketDeleted(t, db, c) + + // Fetch the new log. We should see + // OurOutputIndex matching the testOurIndex + // value, indicating that for migrated logs we + // won't touch them. + // + // NOTE: when the log is created before + // migration, OurOutputIndex would be + // testOurIndex rather than OutputIndexEmpty. + l := fetchNewLog(t, db, c, logHeight1) + require.EqualValues( + t, testOurIndex, l.OurOutputIndex, + "expected log to be NOT overwritten", + ) + + // Fetch the new log. We should see + // TheirOutputIndex matching the testTheirIndex + // value, indicating that for migrated logs we + // won't touch them. + // + // NOTE: when the log is created before + // migration, TheirOutputIndex would be + // testTheirIndex rather than OutputIndexEmpty. + l = fetchNewLog(t, db, c, logHeight2) + require.EqualValues( + t, testTheirIndex, l.TheirOutputIndex, + "expected log to be NOT overwritten", + ) + }, + } + + // case4 defines a case when the chanBucket has both old and new logs, + // which happens when the migration is ongoing. + case4 := &channelTestCase{ + name: "unfinished migration", + setup: func(db kvdb.Backend) error { + return createNotFinished(db, c) + }, + asserter: func(t *testing.T, db kvdb.Backend) { + // Check that the old bucket is removed. + assertOldLogBucketDeleted(t, db, c) + + // Fetch the new log. We should see + // OurOutputIndex matching the testOurIndex + // value, indicating that for migrated logs we + // won't touch them. + // + // NOTE: when the log is created before + // migration, OurOutputIndex would be + // testOurIndex rather than OutputIndexEmpty. + l := fetchNewLog(t, db, c, logHeight1) + require.EqualValues( + t, testOurIndex, l.OurOutputIndex, + "expected log to be NOT overwritten", + ) + + // We expect to have one new log. + l = fetchNewLog(t, db, c, logHeight2) + assertRevocationLog(t, newLog2, l) + }, + unfinished: true, + } + + // case5 defines a case when the chanBucket has no new logs, which + // happens when we haven't migrated anything for this bucket yet. + case5 := &channelTestCase{ + name: "initial migration", + setup: func(db kvdb.Backend) error { + return createNotStarted(db, c) + }, + asserter: assertNewLogs, + unfinished: true, + } + + // Check that the already migrated logs are overwritten. For two + // channels sorted and stored in boltdb, when the first channel has + // unfinished migrations, even channel two has migrated logs, they will + // be overwritten to make sure the data stay consistent. + if overwrite { + case3.name += " overwritten" + case3.asserter = assertNewLogs + + case4.name += " overwritten" + case4.asserter = assertNewLogs + } + + return []*channelTestCase{case1, case2, case3, case4, case5} +} + +// testCase defines a case for a particular db state that we want to test based +// on whether we have one or two peers, one or two channels for each peer, and +// the particular state for each channel. +type testCase struct { + // name has the format: peer: [channel state]. + name string + + // setups is a list of setup functions we'd run sequentially to provide + // the initial db state. + setups []beforeMigrationFunc + + // asserters is a list of assertions we'd perform after the migration + // function has been called. + asserters []afterMigrationFunc + + // unfinished specifies that the test case is testing a case where the + // revocation migration is considered unfinished. This is useful if + // it's used to construct a larger test case where there's a following + // case with a state of finished, we can then test that the revocation + // logs are overwritten even if the state says finished. + unfinished bool +} + +// buildPeerCases builds test cases based on whether we have one or two +// channels saved under this peer. When there's one channel, we have 5 states, +// and when there are two, we have 25 states, a total of 30 cases. +func buildPeerCases(c1, c2 *mig26.OpenChannel, unfinished bool) []*testCase { + testCases := make([]*testCase, 0) + + // Single peer with one channel. + for _, c := range buildChannelCases(c1, unfinished) { + name := fmt.Sprintf("[channel: %s]", c.name) + tc := &testCase{ + name: name, + setups: []beforeMigrationFunc{c.setup}, + asserters: []afterMigrationFunc{c.asserter}, + unfinished: c.unfinished, + } + testCases = append(testCases, tc) + } + + // Single peer with two channels. + testCases = append( + testCases, buildTwoChannelCases(c1, c2, unfinished)..., + ) + + return testCases +} + +// buildTwoChannelCases takes two channels to build test cases that covers all +// combinations of the two channels' state. Since each channel has 5 states, +// this will give us a total 25 states. +func buildTwoChannelCases(c1, c2 *mig26.OpenChannel, + unfinished bool) []*testCase { + + testCases := make([]*testCase, 0) + + // buildCase is a helper closure that contructs a test case based on + // the two smaller test cases. + buildCase := func(tc1, tc2 *channelTestCase) { + setups := make([]beforeMigrationFunc, 0) + setups = append(setups, tc1.setup) + setups = append(setups, tc2.setup) + + asserters := make([]afterMigrationFunc, 0) + asserters = append(asserters, tc1.asserter) + asserters = append(asserters, tc2.asserter) + + // If any of the test cases has unfinished state, the test case + // would have a state of unfinished, indicating any peers after + // this one must overwrite their revocation logs. + unfinished := tc1.unfinished || tc2.unfinished + + name := fmt.Sprintf("[channelOne: %s] [channelTwo: %s]", + tc1.name, tc2.name) + + tc := &testCase{ + name: name, + setups: setups, + asserters: asserters, + unfinished: unfinished, + } + testCases = append(testCases, tc) + } + + // Build channel cases for both of the channels and combine them. + for _, tc1 := range buildChannelCases(c1, unfinished) { + // The second channel's already migrated logs will be + // overwritten if the first channel has unfinished state, which + // are case4 and case5. + unfinished := unfinished || tc1.unfinished + for _, tc2 := range buildChannelCases(c2, unfinished) { + buildCase(tc1, tc2) + } + } + + return testCases +} + +// assertOldLogBucketDeleted asserts that the given channel's old revocation +// log bucket doesn't exist. +func assertOldLogBucketDeleted(t testing.TB, cdb kvdb.Backend, + c *mig26.OpenChannel) { + + var logBucket kvdb.RBucket + err := kvdb.Update(cdb, func(tx kvdb.RwTx) error { + chanBucket, err := mig25.FetchChanBucket(tx, &c.OpenChannel) + if err != nil { + return err + } + + logBucket = chanBucket.NestedReadBucket( + revocationLogBucketDeprecated, + ) + return err + }, func() {}) + + require.NoError(t, err, "read bucket failed") + require.Nil(t, logBucket, "expected old bucket to be deleted") +} + +// fetchNewLog asserts a revocation log can be found using the given updateNum +// for the specified channel. +func fetchNewLog(t testing.TB, cdb kvdb.Backend, + c *mig26.OpenChannel, updateNum uint64) RevocationLog { + + var newLog RevocationLog + err := kvdb.Update(cdb, func(tx kvdb.RwTx) error { + chanBucket, err := mig25.FetchChanBucket(tx, &c.OpenChannel) + if err != nil { + return err + } + + logBucket, err := fetchLogBucket(chanBucket) + if err != nil { + return err + } + + newLog, err = fetchRevocationLog(logBucket, updateNum) + return err + }, func() {}) + + require.NoError(t, err, "failed to query revocation log") + + return newLog +} + +// assertRevocationLog asserts two revocation logs are equal. +func assertRevocationLog(t testing.TB, want, got RevocationLog) { + require.Equal(t, want.OurOutputIndex, got.OurOutputIndex, + "wrong OurOutputIndex") + require.Equal(t, want.TheirOutputIndex, got.TheirOutputIndex, + "wrong TheirOutputIndex") + require.Equal(t, want.CommitTxHash, got.CommitTxHash, + "wrong CommitTxHash") + require.Equal(t, len(want.HTLCEntries), len(got.HTLCEntries), + "wrong HTLCEntries length") + + for i, expectedHTLC := range want.HTLCEntries { + htlc := got.HTLCEntries[i] + require.Equal(t, expectedHTLC.Amt, htlc.Amt, "wrong Amt") + require.Equal(t, expectedHTLC.RHash, htlc.RHash, "wrong RHash") + require.Equal(t, expectedHTLC.Incoming, htlc.Incoming, + "wrong Incoming") + require.Equal(t, expectedHTLC.OutputIndex, htlc.OutputIndex, + "wrong OutputIndex") + require.Equal(t, expectedHTLC.RefundTimeout, htlc.RefundTimeout, + "wrong RefundTimeout") + } +} diff --git a/channeldb/migtest/migtest.go b/channeldb/migtest/migtest.go index ba723fa7d..317312842 100644 --- a/channeldb/migtest/migtest.go +++ b/channeldb/migtest/migtest.go @@ -84,6 +84,45 @@ func ApplyMigration(t *testing.T, } } +// ApplyMigrationWithDb is a helper test function that encapsulates the general +// steps which are needed to properly check the result of applying migration +// function. This function differs from ApplyMigration as it requires the +// supplied migration functions to take a db instance and construct their own +// database transactions. +func ApplyMigrationWithDb(t testing.TB, beforeMigration, afterMigration, + migrationFunc func(db kvdb.Backend) error) { + + t.Helper() + + cdb, cleanUp, err := MakeDB() + defer cleanUp() + if err != nil { + t.Fatal(err) + } + + // beforeMigration usually used for populating the database + // with test data. + if err := beforeMigration(cdb); err != nil { + t.Fatalf("beforeMigration error: %v", err) + } + + // Apply migration. + if err := migrationFunc(cdb); err != nil { + t.Fatalf("migrationFunc error: %v", err) + } + + // If there's no afterMigration, exit here. + if afterMigration == nil { + return + } + + // afterMigration usually used for checking the database state + // and throwing the error if something went wrong. + if err := afterMigration(cdb); err != nil { + t.Fatalf("afterMigration error: %v", err) + } +} + func newError(e interface{}) error { var err error switch e := e.(type) {