package contractcourt import ( "reflect" "testing" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/stretchr/testify/require" ) type incubateTest struct { nOutputs int chanPoint *wire.OutPoint commOutput *kidOutput htlcOutputs []babyOutput err error } // incubateTests holds the test vectors used to test the state transitions of // outputs stored in the nursery store. var incubateTests []incubateTest var chainHash chainhash.Hash // initIncubateTests instantiates the test vectors during package init, which // properly captures the sign descriptors and public keys. func initIncubateTests() { incubateTests = []incubateTest{ { nOutputs: 0, chanPoint: &outPoints[3], }, { nOutputs: 1, chanPoint: &outPoints[0], commOutput: &kidOutputs[0], }, { nOutputs: 4, chanPoint: &outPoints[0], commOutput: &kidOutputs[0], htlcOutputs: babyOutputs, }, } } // TestNurseryStoreInit verifies basic properties of the nursery store before // any modifying calls are made. func TestNurseryStoreInit(t *testing.T) { cdb, err := channeldb.MakeTestDB(t) require.NoError(t, err, "unable to open channel db") ns, err := NewNurseryStore(&chainHash, cdb) require.NoError(t, err, "unable to open nursery store") assertNumChannels(t, ns, 0) assertNumPreschools(t, ns, 0) } // TestNurseryStoreIncubate tests the primary state transitions taken by outputs // in the nursery store. The test is designed to walk both commitment or htlc // outputs through the nursery store, verifying the properties of the // intermediate states. func TestNurseryStoreIncubate(t *testing.T) { cdb, err := channeldb.MakeTestDB(t) require.NoError(t, err, "unable to open channel db") ns, err := NewNurseryStore(&chainHash, cdb) require.NoError(t, err, "unable to open nursery store") for i, test := range incubateTests { // At the beginning of each test, we do not expect to the // nursery store to be tracking any outputs for this channel // point. assertNumChanOutputs(t, ns, test.chanPoint, 0) // Nursery store should be completely empty. assertNumChannels(t, ns, 0) assertNumPreschools(t, ns, 0) // Begin incubating all of the outputs provided in this test // vector. var kids []kidOutput if test.commOutput != nil { kids = append(kids, *test.commOutput) } err = ns.Incubate(kids, test.htlcOutputs) if err != nil { t.Fatalf("unable to incubate outputs"+ "on test #%d: %v", i, err) } // Now that the outputs have been inserted, the nursery store // should see exactly that many outputs under this channel // point. // NOTE: This property should remain intact after every state // change until the channel has been completely removed. assertNumChanOutputs(t, ns, test.chanPoint, test.nOutputs) // If there were no inputs to be incubated, just check that the // no trace of the channel was left. if test.nOutputs == 0 { assertNumChannels(t, ns, 0) continue } // The test vector has a non-zero number of outputs, we will // expect to only see the one channel from this test case. assertNumChannels(t, ns, 1) // The channel should be shown as immature, since none of the // outputs should be graduated directly after being inserted. // It should also be impossible to remove the channel, if it is // also immature. // NOTE: These two tests properties should hold between every // state change until all outputs have been fully graduated. assertChannelMaturity(t, ns, test.chanPoint, false) assertCanRemoveChannel(t, ns, test.chanPoint, false) // Verify that the htlc outputs, if any, reside in the height // index at their first stage CLTV's expiry height. for _, htlcOutput := range test.htlcOutputs { assertCribAtExpiryHeight(t, ns, &htlcOutput) } // If the commitment output was not dust, we will move it from // the preschool bucket to the kindergarten bucket. if test.commOutput != nil { // If the commitment output was not considered dust, we // should see exactly one preschool output in the // nursery store. assertNumPreschools(t, ns, 1) // Now, move the commitment output to the kindergarten // bucket. err = ns.PreschoolToKinder(test.commOutput, 0) if err != test.err { t.Fatalf("unable to move commitment output from "+ "pscl to kndr: %v", err) } // The total number of outputs for this channel should // not have changed, and the kindergarten output should // reside at its maturity height. assertNumChanOutputs(t, ns, test.chanPoint, test.nOutputs) assertKndrAtMaturityHeight(t, ns, test.commOutput) // The total number of channels should not have changed. assertNumChannels(t, ns, 1) // Channel maturity and removal should reflect that the // channel still has non-graduated outputs. assertChannelMaturity(t, ns, test.chanPoint, false) assertCanRemoveChannel(t, ns, test.chanPoint, false) // Moving the preschool output should have no effect on // the placement of crib outputs in the height index. for _, htlcOutput := range test.htlcOutputs { assertCribAtExpiryHeight(t, ns, &htlcOutput) } } // At this point, we should see no more preschool outputs in the // nursery store. Either it was moved to the kindergarten // bucket, or never inserted. assertNumPreschools(t, ns, 0) // If the commitment output is not-dust, we will graduate the // class at its maturity height. if test.commOutput != nil { // Compute the commitment output's maturity height, and // move proceed to graduate that class. maturityHeight := test.commOutput.ConfHeight() + test.commOutput.BlocksToMaturity() err = ns.GraduateKinder(maturityHeight, test.commOutput) if err != nil { t.Fatalf("unable to graduate kindergarten class at "+ "height %d: %v", maturityHeight, err) } // The total number of outputs for this channel should // not have changed, but the kindergarten output should // have been removed from its maturity height. assertNumChanOutputs(t, ns, test.chanPoint, test.nOutputs) assertKndrNotAtMaturityHeight(t, ns, test.commOutput) // The total number of channels should not have changed. assertNumChannels(t, ns, 1) // Moving the preschool output should have no effect on // the placement of crib outputs in the height index. for _, htlcOutput := range test.htlcOutputs { assertCribAtExpiryHeight(t, ns, &htlcOutput) } } // If there are any htlc outputs to incubate, we will walk them // through their two-stage incubation process. if len(test.htlcOutputs) > 0 { for i, htlcOutput := range test.htlcOutputs { // Begin by moving each htlc output from the // crib to kindergarten state. err = ns.CribToKinder(&htlcOutput) if err != nil { t.Fatalf("unable to move htlc output from "+ "crib to kndr: %v", err) } // Number of outputs for this channel should // remain unchanged. assertNumChanOutputs(t, ns, test.chanPoint, test.nOutputs) // If the output hasn't moved to kndr, it should // be at its crib expiry height, otherwise is // should have been removed. for j := range test.htlcOutputs { if j > i { assertCribAtExpiryHeight(t, ns, &test.htlcOutputs[j]) assertKndrNotAtMaturityHeight(t, ns, &test.htlcOutputs[j].kidOutput) } else { assertCribNotAtExpiryHeight(t, ns, &test.htlcOutputs[j]) assertKndrAtMaturityHeight(t, ns, &test.htlcOutputs[j].kidOutput) } } } // Total number of channels in the nursery store should // be the same, no outputs should be marked as // preschool. assertNumChannels(t, ns, 1) assertNumPreschools(t, ns, 0) // Channel should also not be mature, as it we should // still have outputs in kindergarten. assertChannelMaturity(t, ns, test.chanPoint, false) assertCanRemoveChannel(t, ns, test.chanPoint, false) // Now, graduate each htlc kindergarten output, // asserting the invariant number of outputs being // tracked in this channel for _, htlcOutput := range test.htlcOutputs { maturityHeight := htlcOutput.ConfHeight() + htlcOutput.BlocksToMaturity() err = ns.GraduateKinder(maturityHeight, &htlcOutput.kidOutput) if err != nil { t.Fatalf("unable to graduate htlc output "+ "from kndr to grad: %v", err) } assertNumChanOutputs(t, ns, test.chanPoint, test.nOutputs) } } // All outputs have been advanced through the nursery store, but // no attempt has been made to clean up this channel. We expect // to see the same channel remaining, and no kindergarten // outputs. assertNumChannels(t, ns, 1) assertNumPreschools(t, ns, 0) // Since all outputs have now been graduated, the nursery store // should recognize that the channel is mature, and attempting // to remove it should succeed. assertChannelMaturity(t, ns, test.chanPoint, true) assertCanRemoveChannel(t, ns, test.chanPoint, true) // Now that the channel has been removed, the nursery store // should be no channels in the nursery store, and no outputs // being tracked for this channel point. assertNumChannels(t, ns, 0) assertNumChanOutputs(t, ns, test.chanPoint, 0) // If we had a commitment output, ensure it was removed from the // height index. if test.commOutput != nil { assertKndrNotAtMaturityHeight(t, ns, test.commOutput) } // Check that all htlc outputs are no longer stored in their // crib or kindergarten height buckets. for _, htlcOutput := range test.htlcOutputs { assertCribNotAtExpiryHeight(t, ns, &htlcOutput) assertKndrNotAtMaturityHeight(t, ns, &htlcOutput.kidOutput) } // Lastly, there should be no lingering preschool outputs. assertNumPreschools(t, ns, 0) } } // TestNurseryStoreGraduate verifies that the nursery store properly removes // populated entries from the height index as it is purged, and that the last // purged height is set appropriately. func TestNurseryStoreGraduate(t *testing.T) { cdb, err := channeldb.MakeTestDB(t) require.NoError(t, err, "unable to open channel db") ns, err := NewNurseryStore(&chainHash, cdb) require.NoError(t, err, "unable to open nursery store") kid := &kidOutputs[3] // Compute the height at which this output will be inserted in the // height index. maturityHeight := kid.ConfHeight() + kid.BlocksToMaturity() // First, add a commitment output to the nursery store, which is // initially inserted in the preschool bucket. err = ns.Incubate([]kidOutput{*kid}, nil) require.NoError(t, err, "unable to incubate commitment output") // Then, move the commitment output to the kindergarten bucket, such // that it resides in the height index at its maturity height. err = ns.PreschoolToKinder(kid, 0) require.NoError(t, err, "unable to move pscl output to kndr") // Now, iteratively purge all height below the target maturity height, // checking that each class is now empty, and that the last purged // height is set correctly. for i := 0; i < int(maturityHeight); i++ { assertHeightIsPurged(t, ns, uint32(i)) } // Check that the commitment output currently exists at its maturity // height. assertKndrAtMaturityHeight(t, ns, kid) err = ns.GraduateKinder(maturityHeight, kid) if err != nil { t.Fatalf("unable to graduate kindergarten outputs at height=%d: "+ "%v", maturityHeight, err) } assertHeightIsPurged(t, ns, maturityHeight) } // assertNumChanOutputs checks that the channel bucket has the expected number // of outputs. func assertNumChanOutputs(t *testing.T, ns NurseryStorer, chanPoint *wire.OutPoint, expectedNum int) { var count int err := ns.ForChanOutputs(chanPoint, func([]byte, []byte) error { count++ return nil }, func() { count = 0 }) if count == 0 && err == ErrContractNotFound { return } else if err != nil { t.Fatalf("unable to count num outputs for channel %v: %v", chanPoint, err) } if count != expectedNum { t.Fatalf("nursery store should have %d outputs, found %d", expectedNum, count) } } // assertNumPreschools loads all preschool outputs and verifies their count // matches the expected number. func assertNumPreschools(t *testing.T, ns NurseryStorer, expected int) { psclOutputs, err := ns.FetchPreschools() require.NoError(t, err, "unable to retrieve preschool outputs") if len(psclOutputs) != expected { t.Fatalf("expected number of pscl outputs to be %d, got %v", expected, len(psclOutputs)) } } // assertNumChannels checks that the nursery has a given number of active // channels. func assertNumChannels(t *testing.T, ns NurseryStorer, expected int) { channels, err := ns.ListChannels() if err != nil { t.Fatalf("unable to fetch channels from nursery store: %v", err) } if len(channels) != expected { t.Fatalf("expected number of active channels to be %d, got %d", expected, len(channels)) } } // assertHeightIsPurged checks that the finalized transaction, kindergarten, and // htlc outputs at a particular height are all nil. func assertHeightIsPurged(t *testing.T, ns NurseryStorer, height uint32) { kndrOutputs, cribOutputs, err := ns.FetchClass(height) if err != nil { t.Fatalf("unable to retrieve class at height=%d: %v", height, err) } if kndrOutputs != nil { t.Fatalf("height=%d not purged, kndr outputs should be nil", height) } if cribOutputs != nil { t.Fatalf("height=%d not purged, crib outputs should be nil", height) } } // assertCribAtExpiryHeight loads the class at the given height, and verifies // that the given htlc output is one of the crib outputs. func assertCribAtExpiryHeight(t *testing.T, ns NurseryStorer, htlcOutput *babyOutput) { expiryHeight := htlcOutput.expiry _, cribOutputs, err := ns.FetchClass(expiryHeight) if err != nil { t.Fatalf("unable to retrieve class at height=%d: %v", expiryHeight, err) } for _, crib := range cribOutputs { if reflect.DeepEqual(&crib, htlcOutput) { return } } t.Fatalf("could not find crib output %v at height %d", htlcOutput.OutPoint(), expiryHeight) } // assertCribNotAtExpiryHeight loads the class at the given height, and verifies // that the given htlc output is not one of the crib outputs. func assertCribNotAtExpiryHeight(t *testing.T, ns NurseryStorer, htlcOutput *babyOutput) { expiryHeight := htlcOutput.expiry _, cribOutputs, err := ns.FetchClass(expiryHeight) if err != nil { t.Fatalf("unable to retrieve class at height %d: %v", expiryHeight, err) } for _, crib := range cribOutputs { if reflect.DeepEqual(&crib, htlcOutput) { t.Fatalf("found find crib output %v at height %d", htlcOutput.OutPoint(), expiryHeight) } } } // assertKndrAtMaturityHeight loads the class at the provided height and // verifies that the provided kid output is one of the kindergarten outputs // returned. func assertKndrAtMaturityHeight(t *testing.T, ns NurseryStorer, kndrOutput *kidOutput) { maturityHeight := kndrOutput.ConfHeight() + kndrOutput.BlocksToMaturity() kndrOutputs, _, err := ns.FetchClass(maturityHeight) if err != nil { t.Fatalf("unable to retrieve class at height %d: %v", maturityHeight, err) } for _, kndr := range kndrOutputs { if reflect.DeepEqual(&kndr, kndrOutput) { return } } t.Fatalf("could not find kndr output %v at height %d", kndrOutput.OutPoint(), maturityHeight) } // assertKndrNotAtMaturityHeight loads the class at the provided height and // verifies that the provided kid output is not one of the kindergarten outputs // returned. func assertKndrNotAtMaturityHeight(t *testing.T, ns NurseryStorer, kndrOutput *kidOutput) { maturityHeight := kndrOutput.ConfHeight() + kndrOutput.BlocksToMaturity() kndrOutputs, _, err := ns.FetchClass(maturityHeight) if err != nil { t.Fatalf("unable to retrieve class at height %d: %v", maturityHeight, err) } for _, kndr := range kndrOutputs { if reflect.DeepEqual(&kndr, kndrOutput) { t.Fatalf("found find kndr output %v at height %d", kndrOutput.OutPoint(), maturityHeight) } } } // assertChannelMaturity queries the nursery store for the maturity of the given // channel, failing if the result does not match the expectedMaturity. func assertChannelMaturity(t *testing.T, ns NurseryStorer, chanPoint *wire.OutPoint, expectedMaturity bool) { isMature, err := ns.IsMatureChannel(chanPoint) require.NoError(t, err, "unable to fetch channel maturity") if isMature != expectedMaturity { t.Fatalf("expected channel maturity: %v, actual: %v", expectedMaturity, isMature) } } // assertCanRemoveChannel tries to remove a channel from the nursery store, // failing if the result does match expected canRemove. func assertCanRemoveChannel(t *testing.T, ns NurseryStorer, chanPoint *wire.OutPoint, canRemove bool) { err := ns.RemoveChannel(chanPoint) if canRemove && err != nil { t.Fatalf("expected nil when removing active channel, got: %v", err) } else if !canRemove && err != ErrImmatureChannel { t.Fatalf("expected ErrImmatureChannel when removing "+ "active channel: %v", err) } }