556 lines
17 KiB
Go
556 lines
17 KiB
Go
// +build !rpctest
|
|
|
|
package lnd
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/lightningnetwork/lnd/chainreg"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
)
|
|
|
|
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
|
|
|
|
// 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, cleanUp, err := channeldb.MakeTestDB()
|
|
if err != nil {
|
|
t.Fatalf("unable to open channel db: %v", err)
|
|
}
|
|
defer cleanUp()
|
|
|
|
ns, err := newNurseryStore(&chainreg.BitcoinTestnetGenesis, cdb)
|
|
if err != nil {
|
|
t.Fatalf("unable to open nursery store: %v", err)
|
|
}
|
|
|
|
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, cleanUp, err := channeldb.MakeTestDB()
|
|
if err != nil {
|
|
t.Fatalf("unable to open channel db: %v", err)
|
|
}
|
|
defer cleanUp()
|
|
|
|
ns, err := newNurseryStore(&chainreg.BitcoinTestnetGenesis, cdb)
|
|
if err != nil {
|
|
t.Fatalf("unable to open nursery store: %v", err)
|
|
}
|
|
|
|
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, cleanUp, err := channeldb.MakeTestDB()
|
|
if err != nil {
|
|
t.Fatalf("unable to open channel db: %v", err)
|
|
}
|
|
defer cleanUp()
|
|
|
|
ns, err := newNurseryStore(&chainreg.BitcoinTestnetGenesis, cdb)
|
|
if err != nil {
|
|
t.Fatalf("unable to open nursery store: %v", err)
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
t.Fatalf("unable to incubate commitment output: %v", err)
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
t.Fatalf("unable to move pscl output to kndr: %v", err)
|
|
}
|
|
|
|
// 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 NurseryStore,
|
|
chanPoint *wire.OutPoint, expectedNum int) {
|
|
|
|
var count int
|
|
err := ns.ForChanOutputs(chanPoint, func([]byte, []byte) error {
|
|
count++
|
|
return nil
|
|
})
|
|
|
|
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 NurseryStore, expected int) {
|
|
psclOutputs, err := ns.FetchPreschools()
|
|
if err != nil {
|
|
t.Fatalf("unable to retrieve preschool outputs: %v", err)
|
|
}
|
|
|
|
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 NurseryStore, 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 NurseryStore,
|
|
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 NurseryStore,
|
|
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 NurseryStore,
|
|
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 NurseryStore,
|
|
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 NurseryStore,
|
|
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 NurseryStore,
|
|
chanPoint *wire.OutPoint, expectedMaturity bool) {
|
|
|
|
isMature, err := ns.IsMatureChannel(chanPoint)
|
|
if err != nil {
|
|
t.Fatalf("unable to fetch channel maturity: %v", err)
|
|
}
|
|
|
|
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 NurseryStore,
|
|
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)
|
|
}
|
|
}
|