package netann_test import ( "bytes" "crypto/rand" "encoding/binary" "fmt" "io" "sync" "sync/atomic" "testing" "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/wire" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/models" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/netann" "github.com/stretchr/testify/require" ) var ( testKeyLoc = keychain.KeyLocator{Family: keychain.KeyFamilyNodeKey} // testSigBytes specifies a testing signature with the minimal length. testSigBytes = []byte{ 0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00, } ) // randOutpoint creates a random wire.Outpoint. func randOutpoint(t *testing.T) wire.OutPoint { t.Helper() var buf [36]byte _, err := io.ReadFull(rand.Reader, buf[:]) require.NoError(t, err, "unable to generate random outpoint") op := wire.OutPoint{} copy(op.Hash[:], buf[:32]) op.Index = binary.BigEndian.Uint32(buf[32:]) return op } var shortChanIDs uint64 // createChannel generates a channeldb.OpenChannel with a random chanpoint and // short channel id. func createChannel(t *testing.T) *channeldb.OpenChannel { t.Helper() sid := atomic.AddUint64(&shortChanIDs, 1) return &channeldb.OpenChannel{ ShortChannelID: lnwire.NewShortChanIDFromInt(sid), ChannelFlags: lnwire.FFAnnounceChannel, FundingOutpoint: randOutpoint(t), } } // createEdgePolicies generates an edge info and two directional edge policies. // The remote party's public key is generated randomly, and then sorted against // our `pubkey` with the direction bit set appropriately in the policies. Our // update will be created with the disabled bit set if startEnabled is false. func createEdgePolicies(t *testing.T, channel *channeldb.OpenChannel, pubkey *btcec.PublicKey, startEnabled bool) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy) { var ( pubkey1 [33]byte pubkey2 [33]byte dir1 lnwire.ChanUpdateChanFlags dir2 lnwire.ChanUpdateChanFlags ) // Set pubkey1 to OUR pubkey. copy(pubkey1[:], pubkey.SerializeCompressed()) // Set the disabled bit appropriately on our update. if !startEnabled { dir1 |= lnwire.ChanUpdateDisabled } // Generate and set pubkey2 for THEIR pubkey. privKey2, err := btcec.NewPrivateKey() require.NoError(t, err, "unable to generate key pair") copy(pubkey2[:], privKey2.PubKey().SerializeCompressed()) // Set pubkey1 to the lower of the two pubkeys. if bytes.Compare(pubkey2[:], pubkey1[:]) < 0 { pubkey1, pubkey2 = pubkey2, pubkey1 dir1, dir2 = dir2, dir1 } // Now that the ordering has been established, set pubkey2's direction // bit. dir2 |= lnwire.ChanUpdateDirection return &models.ChannelEdgeInfo{ ChannelPoint: channel.FundingOutpoint, NodeKey1Bytes: pubkey1, NodeKey2Bytes: pubkey2, }, &models.ChannelEdgePolicy{ ChannelID: channel.ShortChanID().ToUint64(), ChannelFlags: dir1, LastUpdate: time.Now(), SigBytes: testSigBytes, }, &models.ChannelEdgePolicy{ ChannelID: channel.ShortChanID().ToUint64(), ChannelFlags: dir2, LastUpdate: time.Now(), SigBytes: testSigBytes, } } type mockGraph struct { mu sync.Mutex channels []*channeldb.OpenChannel chanInfos map[wire.OutPoint]*models.ChannelEdgeInfo chanPols1 map[wire.OutPoint]*models.ChannelEdgePolicy chanPols2 map[wire.OutPoint]*models.ChannelEdgePolicy sidToCid map[lnwire.ShortChannelID]wire.OutPoint updates chan *lnwire.ChannelUpdate1 } func newMockGraph(t *testing.T, numChannels int, startActive, startEnabled bool, pubKey *btcec.PublicKey) *mockGraph { g := &mockGraph{ channels: make([]*channeldb.OpenChannel, 0, numChannels), chanInfos: make(map[wire.OutPoint]*models.ChannelEdgeInfo), chanPols1: make(map[wire.OutPoint]*models.ChannelEdgePolicy), chanPols2: make(map[wire.OutPoint]*models.ChannelEdgePolicy), sidToCid: make(map[lnwire.ShortChannelID]wire.OutPoint), updates: make(chan *lnwire.ChannelUpdate1, 2*numChannels), } for i := 0; i < numChannels; i++ { c := createChannel(t) info, pol1, pol2 := createEdgePolicies( t, c, pubKey, startEnabled, ) g.addChannel(c) g.addEdgePolicy(c, info, pol1, pol2) } return g } func (g *mockGraph) FetchAllOpenChannels() ([]*channeldb.OpenChannel, error) { return g.chans(), nil } func (g *mockGraph) FetchChannelEdgesByOutpoint( op *wire.OutPoint) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { g.mu.Lock() defer g.mu.Unlock() info, ok := g.chanInfos[*op] if !ok { return nil, nil, nil, channeldb.ErrEdgeNotFound } pol1 := g.chanPols1[*op] pol2 := g.chanPols2[*op] return info, pol1, pol2, nil } func (g *mockGraph) ApplyChannelUpdate(update *lnwire.ChannelUpdate1, op *wire.OutPoint, private bool) error { g.mu.Lock() defer g.mu.Unlock() outpoint, ok := g.sidToCid[update.ShortChannelID] if !ok { return fmt.Errorf("unknown short channel id: %v", update.ShortChannelID) } pol1 := g.chanPols1[outpoint] pol2 := g.chanPols2[outpoint] // Determine which policy we should update by making the flags on the // policies and updates, and seeing which match up. var update1 bool switch { case update.ChannelFlags&lnwire.ChanUpdateDirection == pol1.ChannelFlags&lnwire.ChanUpdateDirection: update1 = true case update.ChannelFlags&lnwire.ChanUpdateDirection == pol2.ChannelFlags&lnwire.ChanUpdateDirection: update1 = false default: return fmt.Errorf("unable to find policy to update") } timestamp := time.Unix(int64(update.Timestamp), 0) policy := &models.ChannelEdgePolicy{ ChannelID: update.ShortChannelID.ToUint64(), ChannelFlags: update.ChannelFlags, LastUpdate: timestamp, SigBytes: testSigBytes, } if update1 { g.chanPols1[outpoint] = policy } else { g.chanPols2[outpoint] = policy } // Send the update to network. This channel should be sufficiently // buffered to avoid deadlocking. g.updates <- update return nil } func (g *mockGraph) chans() []*channeldb.OpenChannel { g.mu.Lock() defer g.mu.Unlock() channels := make([]*channeldb.OpenChannel, 0, len(g.channels)) channels = append(channels, g.channels...) return channels } func (g *mockGraph) addChannel(channel *channeldb.OpenChannel) { g.mu.Lock() defer g.mu.Unlock() g.channels = append(g.channels, channel) } func (g *mockGraph) addEdgePolicy(c *channeldb.OpenChannel, info *models.ChannelEdgeInfo, pol1, pol2 *models.ChannelEdgePolicy) { g.mu.Lock() defer g.mu.Unlock() g.chanInfos[c.FundingOutpoint] = info g.chanPols1[c.FundingOutpoint] = pol1 g.chanPols2[c.FundingOutpoint] = pol2 g.sidToCid[c.ShortChanID()] = c.FundingOutpoint } func (g *mockGraph) removeChannel(channel *channeldb.OpenChannel) { g.mu.Lock() defer g.mu.Unlock() for i, c := range g.channels { if c.FundingOutpoint != channel.FundingOutpoint { continue } g.channels = append(g.channels[:i], g.channels[i+1:]...) delete(g.chanInfos, c.FundingOutpoint) delete(g.chanPols1, c.FundingOutpoint) delete(g.chanPols2, c.FundingOutpoint) delete(g.sidToCid, c.ShortChanID()) return } } type mockSwitch struct { mu sync.Mutex isActive map[lnwire.ChannelID]bool } func newMockSwitch() *mockSwitch { return &mockSwitch{ isActive: make(map[lnwire.ChannelID]bool), } } func (s *mockSwitch) HasActiveLink(chanID lnwire.ChannelID) bool { s.mu.Lock() defer s.mu.Unlock() // If the link is found, we will returns it's active status. In the // real switch, it returns EligibleToForward(). active, ok := s.isActive[chanID] if ok { return active } return false } func (s *mockSwitch) SetStatus(chanID lnwire.ChannelID, active bool) { s.mu.Lock() defer s.mu.Unlock() s.isActive[chanID] = active } func newManagerCfg(t *testing.T, numChannels int, startEnabled bool) (*netann.ChanStatusConfig, *mockGraph, *mockSwitch) { t.Helper() privKey, err := btcec.NewPrivateKey() require.NoError(t, err, "unable to generate key pair") privKeySigner := keychain.NewPrivKeyMessageSigner(privKey, testKeyLoc) graph := newMockGraph( t, numChannels, startEnabled, startEnabled, privKey.PubKey(), ) htlcSwitch := newMockSwitch() cfg := &netann.ChanStatusConfig{ ChanStatusSampleInterval: 50 * time.Millisecond, ChanEnableTimeout: 500 * time.Millisecond, ChanDisableTimeout: time.Second, OurPubKey: privKey.PubKey(), OurKeyLoc: testKeyLoc, MessageSigner: netann.NewNodeSigner(privKeySigner), IsChannelActive: htlcSwitch.HasActiveLink, ApplyChannelUpdate: graph.ApplyChannelUpdate, DB: graph, Graph: graph, } return cfg, graph, htlcSwitch } type testHarness struct { t *testing.T numChannels int graph *mockGraph htlcSwitch *mockSwitch mgr *netann.ChanStatusManager ourPubKey *btcec.PublicKey safeDisableTimeout time.Duration } // newHarness returns a new testHarness for testing a ChanStatusManager. The // mockGraph will be populated with numChannels channels. The startActive and // startEnabled govern the initial state of the channels wrt the htlcswitch and // the network, respectively. func newHarness(t *testing.T, numChannels int, startActive, startEnabled bool) testHarness { cfg, graph, htlcSwitch := newManagerCfg(t, numChannels, startEnabled) mgr, err := netann.NewChanStatusManager(cfg) require.NoError(t, err, "unable to create chan status manager") err = mgr.Start() require.NoError(t, err, "unable to start chan status manager") h := testHarness{ t: t, numChannels: numChannels, graph: graph, htlcSwitch: htlcSwitch, mgr: mgr, ourPubKey: cfg.OurPubKey, safeDisableTimeout: (3 * cfg.ChanDisableTimeout) / 2, // 1.5x } // Initialize link status as requested. if startActive { h.markActive(h.graph.channels) } else { h.markInactive(h.graph.channels) } return h } // markActive updates the active status of the passed channels within the mock // switch to active. func (h *testHarness) markActive(channels []*channeldb.OpenChannel) { h.t.Helper() for _, channel := range channels { chanID := lnwire.NewChanIDFromOutPoint(channel.FundingOutpoint) h.htlcSwitch.SetStatus(chanID, true) } } // markInactive updates the active status of the passed channels within the mock // switch to inactive. func (h *testHarness) markInactive(channels []*channeldb.OpenChannel) { h.t.Helper() for _, channel := range channels { chanID := lnwire.NewChanIDFromOutPoint(channel.FundingOutpoint) h.htlcSwitch.SetStatus(chanID, false) } } // assertEnables requests enables for all of the passed channels, and asserts // that the errors returned from RequestEnable matches expErr. func (h *testHarness) assertEnables(channels []*channeldb.OpenChannel, expErr error, manual bool) { h.t.Helper() for _, channel := range channels { h.assertEnable(channel.FundingOutpoint, expErr, manual) } } // assertDisables requests disables for all of the passed channels, and asserts // that the errors returned from RequestDisable matches expErr. func (h *testHarness) assertDisables(channels []*channeldb.OpenChannel, expErr error, manual bool) { h.t.Helper() for _, channel := range channels { h.assertDisable(channel.FundingOutpoint, expErr, manual) } } // assertAutos requests auto state management for all of the passed channels, and // asserts that the errors returned from RequestAuto matches expErr. func (h *testHarness) assertAutos(channels []*channeldb.OpenChannel, expErr error) { h.t.Helper() for _, channel := range channels { h.assertAuto(channel.FundingOutpoint, expErr) } } // assertEnable requests an enable for the given outpoint, and asserts that the // returned error matches expErr. func (h *testHarness) assertEnable(outpoint wire.OutPoint, expErr error, manual bool) { h.t.Helper() err := h.mgr.RequestEnable(outpoint, manual) if err != expErr { h.t.Fatalf("expected enable error: %v, got %v", expErr, err) } } // assertDisable requests a disable for the given outpoint, and asserts that the // returned error matches expErr. func (h *testHarness) assertDisable(outpoint wire.OutPoint, expErr error, manual bool) { h.t.Helper() err := h.mgr.RequestDisable(outpoint, manual) if err != expErr { h.t.Fatalf("expected disable error: %v, got %v", expErr, err) } } // assertAuto requests auto state management for the given outpoint, and asserts // that the returned error matches expErr. func (h *testHarness) assertAuto(outpoint wire.OutPoint, expErr error) { h.t.Helper() err := h.mgr.RequestAuto(outpoint) if err != expErr { h.t.Fatalf("expected error: %v, got %v", expErr, err) } } // assertNoUpdates waits for the specified duration, and asserts that no updates // are announced on the network. func (h *testHarness) assertNoUpdates(duration time.Duration) { h.t.Helper() h.assertUpdates(nil, false, duration) } // assertUpdates waits for the specified duration, asserting that an update // are receive on the network for each of the passed OpenChannels, and that all // of their disable bits are set to match expEnabled. The expEnabled parameter // is ignored if channels is nil. func (h *testHarness) assertUpdates(channels []*channeldb.OpenChannel, expEnabled bool, duration time.Duration) { h.t.Helper() // Compute an index of the expected short channel ids for which we want // to received updates. expSids := sidsFromChans(channels) timeout := time.After(duration) recvdSids := make(map[lnwire.ShortChannelID]struct{}) for { select { case upd := <-h.graph.updates: // Assert that the received short channel id is one that // we expect. If no updates were expected, this will // always fail on the first update received. if _, ok := expSids[upd.ShortChannelID]; !ok { h.t.Fatalf("received update for unexpected "+ "short chan id: %v", upd.ShortChannelID) } // Assert that the disabled bit is set properly. enabled := upd.ChannelFlags&lnwire.ChanUpdateDisabled != lnwire.ChanUpdateDisabled if expEnabled != enabled { h.t.Fatalf("expected enabled: %v, actual: %v", expEnabled, enabled) } recvdSids[upd.ShortChannelID] = struct{}{} case <-timeout: // Time is up, assert that the correct number of unique // updates was received. if len(recvdSids) == len(channels) { return } h.t.Fatalf("expected %d updates, got %d", len(channels), len(recvdSids)) } } } // sidsFromChans returns an index contain the short channel ids of each channel // provided in the list of OpenChannels. func sidsFromChans( channels []*channeldb.OpenChannel) map[lnwire.ShortChannelID]struct{} { sids := make(map[lnwire.ShortChannelID]struct{}) for _, channel := range channels { sids[channel.ShortChanID()] = struct{}{} } return sids } type stateMachineTest struct { name string startEnabled bool startActive bool fn func(testHarness) } var stateMachineTests = []stateMachineTest{ { name: "active and enabled is stable", startActive: true, startEnabled: true, fn: func(h testHarness) { // No updates should be sent because being active and // enabled should be a stable state. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "inactive and disabled is stable", startActive: false, startEnabled: false, fn: func(h testHarness) { // No updates should be sent because being inactive and // disabled should be a stable state. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "start disabled request enable", startActive: true, // can't request enable unless active startEnabled: false, fn: func(h testHarness) { // Request enables for all channels. h.assertEnables(h.graph.chans(), nil, false) // Expect to see them all enabled on the network. h.assertUpdates( h.graph.chans(), true, h.safeDisableTimeout, ) }, }, { name: "start enabled request disable", startActive: true, startEnabled: true, fn: func(h testHarness) { // Request disables for all channels. h.assertDisables(h.graph.chans(), nil, false) // Expect to see them all disabled on the network. h.assertUpdates( h.graph.chans(), false, h.safeDisableTimeout, ) }, }, { name: "request enable already enabled", startActive: true, startEnabled: true, fn: func(h testHarness) { // Request enables for already enabled channels. h.assertEnables(h.graph.chans(), nil, false) // Manager shouldn't send out any updates. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "request disabled already disabled", startActive: false, startEnabled: false, fn: func(h testHarness) { // Request disables for already enabled channels. h.assertDisables(h.graph.chans(), nil, false) // Manager shouldn't sent out any updates. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "detect and disable inactive", startActive: true, startEnabled: true, fn: func(h testHarness) { // Simulate disconnection and have links go inactive. h.markInactive(h.graph.chans()) // Should see all channels passively disabled. h.assertUpdates( h.graph.chans(), false, h.safeDisableTimeout, ) }, }, { name: "quick flap stays active", startActive: true, startEnabled: true, fn: func(h testHarness) { // Simulate disconnection and have links go inactive. h.markInactive(h.graph.chans()) // Allow 2 sample intervals to pass, but not long // enough for a disable to occur. time.Sleep(100 * time.Millisecond) // Simulate reconnect by making channels active. h.markActive(h.graph.chans()) // Request that all channels be re-enabled. h.assertEnables(h.graph.chans(), nil, false) // Pending disable should have been canceled, and // no updates sent. Channels remain enabled on the // network. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "no passive enable from becoming active", startActive: false, startEnabled: false, fn: func(h testHarness) { // Simulate reconnect by making channels active. h.markActive(h.graph.chans()) // No updates should be sent without explicit enable. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "enable inactive channel fails", startActive: false, startEnabled: false, fn: func(h testHarness) { // Request enable of inactive channels, expect error // indicating that channel was not active. h.assertEnables( h.graph.chans(), netann.ErrEnableInactiveChan, false, ) // No updates should be sent as a result of the failure. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "enable unknown channel fails", startActive: false, startEnabled: false, fn: func(h testHarness) { // Create channels unknown to the graph. unknownChans := []*channeldb.OpenChannel{ createChannel(h.t), createChannel(h.t), createChannel(h.t), } // Request that they be enabled, which should return an // error as the graph doesn't have an edge for them. h.assertEnables( unknownChans, channeldb.ErrEdgeNotFound, false, ) // No updates should be sent as a result of the failure. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "disable unknown channel fails", startActive: false, startEnabled: false, fn: func(h testHarness) { // Create channels unknown to the graph. unknownChans := []*channeldb.OpenChannel{ createChannel(h.t), createChannel(h.t), createChannel(h.t), } // Request that they be disabled, which should return an // error as the graph doesn't have an edge for them. h.assertDisables( unknownChans, channeldb.ErrEdgeNotFound, false, ) // No updates should be sent as a result of the failure. h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "add new channels", startActive: false, startEnabled: false, fn: func(h testHarness) { // Allow the manager to enter a steady state for the // initial channel set. h.assertNoUpdates(h.safeDisableTimeout) // Add a new channels to the graph, but don't yet add // the edge policies. We should see no updates sent // since the manager can't access the policies. newChans := []*channeldb.OpenChannel{ createChannel(h.t), createChannel(h.t), createChannel(h.t), } for _, c := range newChans { h.graph.addChannel(c) } h.assertNoUpdates(h.safeDisableTimeout) // Check that trying to enable the channel with unknown // edges results in a failure. h.assertEnables(newChans, channeldb.ErrEdgeNotFound, false) // Now, insert edge policies for the channel into the // graph, starting with the channel enabled, and mark // the link active. for _, c := range newChans { info, pol1, pol2 := createEdgePolicies( h.t, c, h.ourPubKey, true, ) h.graph.addEdgePolicy(c, info, pol1, pol2) } h.markActive(newChans) // We expect no updates to be sent since the channel is // enabled and active. h.assertNoUpdates(h.safeDisableTimeout) // Finally, assert that enabling the channel doesn't // return an error now that everything is in place. h.assertEnables(newChans, nil, false) }, }, { name: "remove channels then disable", startActive: true, startEnabled: true, fn: func(h testHarness) { // Allow the manager to enter a steady state for the // initial channel set. h.assertNoUpdates(h.safeDisableTimeout) // Select half of the current channels to remove. channels := h.graph.chans() rmChans := channels[:len(channels)/2] // Mark the channel inactive and remove them from the // graph. This should trigger the manager to attempt a // mark the channel disabled, but will unable to do so // because it can't find the edge policies. h.markInactive(rmChans) for _, c := range rmChans { h.graph.removeChannel(c) } h.assertNoUpdates(h.safeDisableTimeout) // Check that trying to enable the channel with unknown // edges results in a failure. h.assertDisables(rmChans, channeldb.ErrEdgeNotFound, false) }, }, { name: "disable channels then remove", startActive: true, startEnabled: true, fn: func(h testHarness) { // Allow the manager to enter a steady state for the // initial channel set. h.assertNoUpdates(h.safeDisableTimeout) // Select half of the current channels to remove. channels := h.graph.chans() rmChans := channels[:len(channels)/2] // Check that trying to enable the channel with unknown // edges results in a failure. h.assertDisables(rmChans, nil, false) // Since the channels are still in the graph, we expect // these channels to be disabled on the network. h.assertUpdates(rmChans, false, h.safeDisableTimeout) // Finally, remove the channels from the graph and // assert no more updates are sent. for _, c := range rmChans { h.graph.removeChannel(c) } h.assertNoUpdates(h.safeDisableTimeout) }, }, { name: "request manual enable", startActive: true, startEnabled: false, fn: func(h testHarness) { // Request manual enables for all channels. h.assertEnables(h.graph.chans(), nil, true) // Expect to see them all enabled on the network. h.assertUpdates( h.graph.chans(), true, h.safeDisableTimeout, ) // Subsequent request disables with manual = false should succeed. h.assertDisables( h.graph.chans(), nil, false, ) // Expect to see them all disabled on the network again. h.assertUpdates( h.graph.chans(), false, h.safeDisableTimeout, ) }, }, { name: "request manual disable", startActive: true, startEnabled: true, fn: func(h testHarness) { // Request manual disables for all channels. h.assertDisables(h.graph.chans(), nil, true) // Expect to see them all disabled on the network. h.assertUpdates( h.graph.chans(), false, h.safeDisableTimeout, ) // Request enables with manual = false should fail. h.assertEnables( h.graph.chans(), netann.ErrEnableManuallyDisabledChan, false, ) // Request enables with manual = true should succeed. h.assertEnables(h.graph.chans(), nil, true) // Expect to see them all enabled on the network again. h.assertUpdates( h.graph.chans(), true, h.safeDisableTimeout, ) }, }, { name: "restore auto", startActive: true, startEnabled: true, fn: func(h testHarness) { // Request manual disables for all channels. h.assertDisables(h.graph.chans(), nil, true) // Expect to see them all disabled on the network. h.assertUpdates( h.graph.chans(), false, h.safeDisableTimeout, ) // Request enables with manual = false should fail. h.assertEnables( h.graph.chans(), netann.ErrEnableManuallyDisabledChan, false, ) // Request enables with manual = false should succeed after // restoring auto state management. h.assertAutos(h.graph.chans(), nil) h.assertEnables(h.graph.chans(), nil, false) // Expect to see them all enabled on the network again. h.assertUpdates( h.graph.chans(), true, h.safeDisableTimeout, ) }, }, } // TestChanStatusManagerStateMachine tests the possible state transitions that // can be taken by the ChanStatusManager. func TestChanStatusManagerStateMachine(t *testing.T) { t.Parallel() for _, test := range stateMachineTests { tc := test t.Run(test.name, func(t *testing.T) { t.Parallel() const numChannels = 10 h := newHarness( t, numChannels, tc.startActive, tc.startEnabled, ) defer h.mgr.Stop() tc.fn(h) }) } }