package lntest import ( "context" "fmt" "sync" "sync/atomic" "testing" "github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lntest/node" "github.com/lightningnetwork/lnd/lntest/wait" ) // nodeManager is responsible for hanlding the start and stop of a given node. // It also keeps track of the running nodes. type nodeManager struct { sync.Mutex // chainBackend houses the information necessary to use a node as LND // chain backend, such as rpc configuration, P2P information etc. chainBackend node.BackendConfig // currentTestCase holds the name for the currently run test case. currentTestCase string // lndBinary is the full path to the lnd binary that was specifically // compiled with all required itest flags. lndBinary string // dbBackend sets the database backend to use. dbBackend node.DatabaseBackend // activeNodes is a map of all running nodes, format: // {pubkey: *HarnessNode}. activeNodes map[uint32]*node.HarnessNode // standbyNodes is a map of all the standby nodes, format: // {pubkey: *HarnessNode}. standbyNodes map[uint32]*node.HarnessNode // nodeCounter is a monotonically increasing counter that's used as the // node's unique ID. nodeCounter uint32 // feeServiceURL is the url of the fee service. feeServiceURL string } // newNodeManager creates a new node manager instance. func newNodeManager(lndBinary string, dbBackend node.DatabaseBackend) *nodeManager { return &nodeManager{ lndBinary: lndBinary, dbBackend: dbBackend, activeNodes: make(map[uint32]*node.HarnessNode), standbyNodes: make(map[uint32]*node.HarnessNode), } } // nextNodeID generates a unique sequence to be used as the node's ID. func (nm *nodeManager) nextNodeID() uint32 { nodeID := atomic.AddUint32(&nm.nodeCounter, 1) return nodeID - 1 } // newNode initializes a new HarnessNode, supporting the ability to initialize // a wallet with or without a seed. If useSeed is false, the returned harness // node can be used immediately. Otherwise, the node will require an additional // initialization phase where the wallet is either created or restored. func (nm *nodeManager) newNode(t *testing.T, name string, extraArgs []string, password []byte, noAuth bool) (*node.HarnessNode, error) { cfg := &node.BaseNodeConfig{ Name: name, LogFilenamePrefix: nm.currentTestCase, Password: password, BackendCfg: nm.chainBackend, ExtraArgs: extraArgs, FeeURL: nm.feeServiceURL, DBBackend: nm.dbBackend, NodeID: nm.nextNodeID(), LndBinary: nm.lndBinary, NetParams: harnessNetParams, SkipUnlock: noAuth, } node, err := node.NewHarnessNode(t, cfg) if err != nil { return nil, err } // Put node in activeNodes to ensure Shutdown is called even if start // returns an error. nm.registerNode(node) return node, nil } // RegisterNode records a new HarnessNode in the NetworkHarnesses map of known // nodes. This method should only be called with nodes that have successfully // retrieved their public keys via FetchNodeInfo. func (nm *nodeManager) registerNode(node *node.HarnessNode) { nm.Lock() nm.activeNodes[node.Cfg.NodeID] = node nm.Unlock() } // ShutdownNode stops an active lnd process and returns when the process has // exited and any temporary directories have been cleaned up. func (nm *nodeManager) shutdownNode(node *node.HarnessNode) error { if err := node.Shutdown(); err != nil { return err } delete(nm.activeNodes, node.Cfg.NodeID) return nil } // restartNode attempts to restart a lightning node by shutting it down // cleanly, then restarting the process. This function is fully blocking. Upon // restart, the RPC connection to the node will be re-attempted, continuing iff // the connection attempt is successful. If the callback parameter is non-nil, // then the function will be executed after the node shuts down, but *before* // the process has been started up again. func (nm *nodeManager) restartNode(ctxt context.Context, hn *node.HarnessNode, callback func() error) error { // Stop the node. if err := hn.Stop(); err != nil { return fmt.Errorf("restart node got error: %w", err) } if callback != nil { if err := callback(); err != nil { return err } } // Start the node without unlocking the wallet. if hn.Cfg.SkipUnlock { return hn.StartWithNoAuth(ctxt) } return hn.Start(ctxt) } // unlockNode unlocks the node's wallet if the password is configured. // Additionally, each time the node is unlocked, the caller can pass a set of // SCBs to pass in via the Unlock method allowing them to restore channels // during restart. func (nm *nodeManager) unlockNode(hn *node.HarnessNode, chanBackups ...*lnrpc.ChanBackupSnapshot) error { // If the node doesn't have a password set, then we can exit here as we // don't need to unlock it. if len(hn.Cfg.Password) == 0 { return nil } // Otherwise, we'll unlock the wallet, then complete the final steps // for the node initialization process. unlockReq := &lnrpc.UnlockWalletRequest{ WalletPassword: hn.Cfg.Password, } if len(chanBackups) != 0 { unlockReq.ChannelBackups = chanBackups[0] unlockReq.RecoveryWindow = 100 } err := wait.NoError(func() error { return hn.Unlock(unlockReq) }, DefaultTimeout) if err != nil { return fmt.Errorf("%s: failed to unlock: %w", hn.Name(), err) } return nil } // initWalletAndNode will unlock the node's wallet and finish setting up the // node so it's ready to take RPC requests. func (nm *nodeManager) initWalletAndNode(hn *node.HarnessNode, req *lnrpc.InitWalletRequest) ([]byte, error) { // Pass the init request via rpc to finish unlocking the node. resp := hn.RPC.InitWallet(req) // Now that the wallet is unlocked, before creating an authed // connection we will close the old unauthed connection. if err := hn.CloseConn(); err != nil { return nil, fmt.Errorf("close unauthed conn failed") } // Init the node, which will create the authed grpc conn and all its // rpc clients. err := hn.InitNode(resp.AdminMacaroon) // In stateless initialization mode we get a macaroon back that we have // to return to the test, otherwise gRPC calls won't be possible since // there are no macaroon files created in that mode. // In stateful init the admin macaroon will just be nil. return resp.AdminMacaroon, err }