diff --git a/Makefile b/Makefile index ccf95cb902f..8fd0f197b19 100644 --- a/Makefile +++ b/Makefile @@ -112,6 +112,7 @@ build: @$(call print, "Building debug lnd and lncli.") $(GOBUILD) -tags="$(DEV_TAGS)" -o lnd-debug $(DEV_GCFLAGS) $(DEV_LDFLAGS) $(PKG)/cmd/lnd $(GOBUILD) -tags="$(DEV_TAGS)" -o lncli-debug $(DEV_GCFLAGS) $(DEV_LDFLAGS) $(PKG)/cmd/lncli + $(GOBUILD) -tags="$(DEV_TAGS)" -o multinode-debug $(DEV_GCFLAGS) $(DEV_LDFLAGS) $(PKG)/cmd/multinode #? build-itest: Build integration test binaries, place them in itest directory build-itest: @@ -136,6 +137,7 @@ install-binaries: @$(call print, "Installing lnd and lncli.") $(GOINSTALL) -tags="${tags}" -ldflags="$(RELEASE_LDFLAGS)" $(PKG)/cmd/lnd $(GOINSTALL) -tags="${tags}" -ldflags="$(RELEASE_LDFLAGS)" $(PKG)/cmd/lncli + $(GOINSTALL) -tags="${tags}" -ldflags="$(RELEASE_LDFLAGS)" $(PKG)/cmd/multinode #? manpages: generate and install man pages manpages: diff --git a/cmd/multinode/main.go b/cmd/multinode/main.go new file mode 100644 index 00000000000..4bbffe910c2 --- /dev/null +++ b/cmd/multinode/main.go @@ -0,0 +1,111 @@ +package main + +import ( + "context" + "fmt" + "os" + + "github.com/jessevdk/go-flags" + "github.com/lightningnetwork/lnd" + graphsources "github.com/lightningnetwork/lnd/graph/sources" + "github.com/lightningnetwork/lnd/signal" +) + +func main() { + // Hook interceptor for os signals. + shutdownInterceptor, err := signal.Intercept() + if err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + lndProviders := setupGraphSourceNode(shutdownInterceptor) + setupDependentNode(shutdownInterceptor, lndProviders) + <-shutdownInterceptor.ShutdownChannel() +} +func setupGraphSourceNode(interceptor signal.Interceptor) lnd.Providers { + preCfg := graphConfig() + cfg, err := lnd.LoadConfigNoFlags(*preCfg, interceptor) + if err != nil { + os.Exit(1) + } + implCfg := cfg.ImplementationConfig(interceptor) + lndProviders := make(chan lnd.Providers, 1) + go func() { + if err := lnd.Main( + cfg, lnd.ListenerCfg{}, implCfg, interceptor, + lndProviders, + ); err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + } + fmt.Println("Graph node has stopped") + os.Exit(1) + }() + return <-lndProviders +} +func setupDependentNode(interceptor signal.Interceptor, + lndProviders lnd.Providers) { + // Load the configuration, and parse any command line options. This + // function will also set up logging properly. + loadedConfig, err := lnd.LoadConfig(interceptor) + if err != nil { + if e, ok := err.(*flags.Error); !ok || e.Type != flags.ErrHelp { + // Print error if not due to help request. + err = fmt.Errorf("failed to load config: %w", err) + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } + // Help was requested, exit normally. + os.Exit(0) + } + loadedConfig.Gossip.NoSync = true + implCfg := loadedConfig.ImplementationConfig(interceptor) + implCfg.GraphProvider = &graphProvider{lndProviders: lndProviders} + // Call the "real" main in a nested manner so the defers will properly + // be executed in the case of a graceful shutdown. + if err = lnd.Main( + loadedConfig, lnd.ListenerCfg{}, implCfg, interceptor, nil, + ); err != nil { + _, _ = fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +type graphProvider struct { + lndProviders lnd.Providers +} + +func (g *graphProvider) Graph(_ context.Context, + dbs *lnd.DatabaseInstances) (graphsources.GraphSource, error) { + + graphSource, err := g.lndProviders.GraphSource() + if err != nil { + return nil, err + } + + return NewGraphBackend( + graphsources.NewDBGSource(dbs.GraphDB), graphSource, + ), nil +} +func graphConfig() *lnd.Config { + cfg := lnd.DefaultConfig() + //if _, err := flags.Parse(&cfg); err != nil { + // os.Exit(1) + //} + cfg.Bitcoin.RegTest = true + cfg.LndDir = "/Users/elle/.lnd-dev-graph" + cfg.BitcoindMode.RPCHost = "localhost:18443" + cfg.Bitcoin.Node = "bitcoind" + cfg.RawRPCListeners = []string{"localhost:10020"} + cfg.BitcoindMode.RPCUser = "lightning" + cfg.BitcoindMode.RPCPass = "lightning" + cfg.BitcoindMode.ZMQPubRawBlock = "tcp://localhost:28332" + cfg.BitcoindMode.ZMQPubRawTx = "tcp://localhost:28333" + cfg.TrickleDelay = 50 + cfg.NoSeedBackup = true + cfg.RawRESTListeners = []string{"localhost:11020"} + cfg.RawListeners = []string{"localhost:9736"} + cfg.DebugLevel = "debug" + cfg.LogConfig.Console.Disable = true + + return &cfg +} diff --git a/cmd/multinode/mux.go b/cmd/multinode/mux.go new file mode 100644 index 00000000000..b1d54eb62b1 --- /dev/null +++ b/cmd/multinode/mux.go @@ -0,0 +1,517 @@ +package main + +import ( + "bytes" + "context" + "fmt" + "net" + "sync" + "time" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/wire" + "github.com/go-errors/errors" + "github.com/lightningnetwork/lnd/autopilot" + "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/discovery" + graphdb "github.com/lightningnetwork/lnd/graph/db" + "github.com/lightningnetwork/lnd/graph/db/models" + graphsession "github.com/lightningnetwork/lnd/graph/session" + graphsources "github.com/lightningnetwork/lnd/graph/sources" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/routing/route" +) + +type GraphSourceMux struct { + remote graphsources.GraphSource + + local *graphsources.DBSource + + // srcPub is a cached version of the local nodes own pub key bytes. The + // mu mutex should be used when accessing this field. + srcPub *route.Vertex + mu sync.Mutex +} + +// A compile-time check to ensure that GraphSourceMux implements GraphSource. +var _ graphsources.GraphSource = (*GraphSourceMux)(nil) + +func NewGraphBackend(local *graphsources.DBSource, + remote graphsources.GraphSource) *GraphSourceMux { + + return &GraphSourceMux{ + local: local, + remote: remote, + } +} + +func (g *GraphSourceMux) NetworkStats(ctx context.Context, + excludeNodes map[route.Vertex]struct{}, + excludeChannels map[uint64]struct{}) (*models.NetworkStats, error) { + + // TODO(elle): need to call local first & build exclude lists to send to + // remote. + return g.remote.NetworkStats(ctx, excludeNodes, excludeChannels) +} + +func (g *GraphSourceMux) GraphBootstrapper(ctx context.Context) ( + discovery.NetworkPeerBootstrapper, error) { + + return g.remote.GraphBootstrapper(ctx) +} + +func (g *GraphSourceMux) BetweennessCentrality(ctx context.Context) ( + map[autopilot.NodeID]*models.BetweennessCentrality, error) { + + return g.remote.BetweennessCentrality(ctx) +} + +// NewPathFindTx returns a new read transaction that can be used other read +// calls to the backing graph. +// +// NOTE: this is part of the graphsession.ReadOnlyGraph interface. +func (g *GraphSourceMux) NewPathFindTx(ctx context.Context) (graphsession.RTx, + error) { + + return newRTxSet(ctx, g.local, g.remote) +} + +// ForEachNodeDirectedChannel iterates through all channels of a given +// node, executing the passed callback on the directed edge representing +// the channel and its incoming policy. +// +// If the node in question is the local node, then only the local node is +// queried since it will know all channels that it owns. +// +// Otherwise, we still query the local node in case the node in question is a +// peer with whom the local node has a private channel to. In that case we want +// to make sure to run the call-back on these directed channels since the remote +// node may not know of this channel. Finally, we call the remote node but skip +// any channels we have already handled. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) ForEachNodeDirectedChannel(ctx context.Context, + tx graphsession.RTx, node route.Vertex, + cb func(channel *graphdb.DirectedChannel) error) error { + + srcPub, err := g.selfNodePub() + if err != nil { + return err + } + + lTx, rTx, err := extractRTxSet(tx) + if err != nil { + return err + } + + // If we are the source node, we know all our channels, so just use + // local DB. + if bytes.Equal(srcPub[:], node[:]) { + return g.local.ForEachNodeDirectedChannel(ctx, lTx, node, cb) + } + + // Call our local DB to collect any private channels we have. + handledPeerChans := make(map[uint64]bool) + err = g.local.ForEachNodeDirectedChannel(ctx, lTx, node, + func(channel *graphdb.DirectedChannel) error { + + // If the other node is not us, we don't need to handle + // it here since the remote node will handle it later. + if !bytes.Equal(channel.OtherNode[:], srcPub[:]) { + return nil + } + + // Else, we call the call back ourselves on this + // channel and mark that we have handled it. + handledPeerChans[channel.ChannelID] = true + + return cb(channel) + }) + if err != nil { + return err + } + + return g.remote.ForEachNodeDirectedChannel(ctx, rTx, node, + func(channel *graphdb.DirectedChannel) error { + + // Skip any we have already handled. + if handledPeerChans[channel.ChannelID] { + return nil + } + + return cb(channel) + }, + ) +} + +// FetchNodeFeatures returns the features of a given node. If no features are +// known for the node, an empty feature vector is returned. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) FetchNodeFeatures(ctx context.Context, + tx graphsession.RTx, node route.Vertex) (*lnwire.FeatureVector, error) { + + lTx, rTx, err := extractRTxSet(tx) + if err != nil { + return nil, err + } + + // Query the local DB first. If a non-empty set of features is returned, + // we use these. Otherwise, the remote DB is checked. + feats, err := g.local.FetchNodeFeatures(ctx, lTx, node) + if err != nil { + return nil, err + } + + if !feats.IsEmpty() { + return feats, nil + } + + return g.remote.FetchNodeFeatures(ctx, rTx, node) +} + +// ForEachNode iterates through all the stored vertices/nodes in the graph, +// executing the passed callback with each node encountered. If the callback +// returns an error, then the transaction is aborted and the iteration stops +// early. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) ForEachNode(ctx context.Context, + cb func(*models.LightningNode) error) error { + + handled := make(map[route.Vertex]struct{}) + + // First cover all the nodes we do know about. We might know of some the + // remote node does not. + err := g.local.ForEachNode(ctx, func(node *models.LightningNode) error { + handled[node.PubKeyBytes] = struct{}{} + + return cb(node) + }) + if err != nil { + return err + } + + return g.remote.ForEachNode(ctx, + func(node *models.LightningNode) error { + if _, ok := handled[node.PubKeyBytes]; ok { + return nil + } + + return cb(node) + }, + ) +} + +// FetchLightningNode attempts to look up a target node by its identity public +// key. If the node isn't found in the database, then ErrGraphNodeNotFound is +// returned. An optional transaction may be provided. If none is provided, then +// a new one will be created. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) FetchLightningNode(ctx context.Context, + nodePub route.Vertex) (*models.LightningNode, error) { + + srcPub, err := g.selfNodePub() + if err != nil { + return nil, err + } + + if bytes.Equal(srcPub[:], nodePub[:]) { + return g.local.FetchLightningNode(ctx, nodePub) + } + + return g.remote.FetchLightningNode(ctx, nodePub) +} + +// ForEachNodeChannel iterates through all channels of the given node, +// executing the passed callback with an edge info structure and the policies +// of each end of the channel. The first edge policy is the outgoing edge *to* +// the connecting node, while the second is the incoming edge *from* the +// connecting node. If the callback returns an error, then the iteration is +// halted with the error propagated back up to the caller. +// +// Unknown policies are passed into the callback as nil values. +// +// If the caller wishes to re-use an existing boltdb transaction, then it +// should be passed as the first argument. Otherwise, the first argument should +// be nil and a fresh transaction will be created to execute the graph +// traversal. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) ForEachNodeChannel(ctx context.Context, + nodePub route.Vertex, cb func(*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error) error { + + // First query our own db since we may have chan info that our remote + // does not know of (regarding our selves or our channel peers). + var found bool + err := g.local.ForEachNodeChannel(ctx, nodePub, func( + info *models.ChannelEdgeInfo, policy *models.ChannelEdgePolicy, + policy2 *models.ChannelEdgePolicy) error { + + found = true + + return cb(info, policy, policy2) + }) + // Only return the error if it was found. + if err != nil && found { + return err + } + + if found { + return nil + } + + return g.remote.ForEachNodeChannel(ctx, nodePub, cb) +} + +// FetchChannelEdgesByID attempts to look up the two directed edges for the +// channel identified by the channel ID. If the channel can't be found, then +// graphdb.ErrEdgeNotFound is returned. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) FetchChannelEdgesByID(ctx context.Context, + chanID uint64) (*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy, error) { + + info, p1, p2, err := g.local.FetchChannelEdgesByID(ctx, chanID) + if err == nil { + return info, p1, p2, nil + } + + return g.remote.FetchChannelEdgesByID(ctx, chanID) +} + +// IsPublicNode is a helper method that determines whether the node with the +// given public key is seen as a public node in the graph from the graph's +// source node's point of view. This first checks the local node and then the +// remote if the node is not seen as public by the loca node. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) IsPublicNode(ctx context.Context, pubKey [33]byte) ( + bool, error) { + + isPublic, err := g.local.IsPublicNode(ctx, pubKey) + if err != nil && !errors.Is(err, graphdb.ErrGraphNodeNotFound) { + return false, err + } + if isPublic { + return true, nil + } + + return g.remote.IsPublicNode(ctx, pubKey) +} + +// FetchChannelEdgesByOutpoint returns the channel edge info and most recent +// channel edge policies for a given outpoint. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) FetchChannelEdgesByOutpoint(ctx context.Context, + point *wire.OutPoint) (*models.ChannelEdgeInfo, + *models.ChannelEdgePolicy, *models.ChannelEdgePolicy, error) { + + edge, p1, p2, err := g.local.FetchChannelEdgesByOutpoint(ctx, point) + if err == nil { + return edge, p1, p2, nil + } + + return g.remote.FetchChannelEdgesByOutpoint(ctx, point) +} + +// AddrsForNode returns all known addresses for the target node public key. The +// returned boolean must indicate if the given node is unknown to the backing +// source. This merges the results from both the local and remote source. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) AddrsForNode(ctx context.Context, + nodePub *btcec.PublicKey) (bool, []net.Addr, error) { + + // Check both the local and remote sources and merge the results. + return channeldb.NewMultiAddrSource( + g.local, g.remote, + ).AddrsForNode(ctx, nodePub) +} + +// ForEachChannel iterates through all the channel edges stored within the graph +// and invokes the passed callback for each edge. If the callback returns an +// error, then the transaction is aborted and the iteration stops early. An +// edge's policy structs may be nil if the ChannelUpdate in question has not yet +// been received for the channel. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) ForEachChannel(ctx context.Context, + cb func(*models.ChannelEdgeInfo, *models.ChannelEdgePolicy, + *models.ChannelEdgePolicy) error) error { + + srcPub, err := g.selfNodePub() + if err != nil { + return err + } + + ourChans := make(map[uint64]bool) + err = g.local.ForEachNodeChannel(ctx, srcPub, func( + info *models.ChannelEdgeInfo, policy *models.ChannelEdgePolicy, + policy2 *models.ChannelEdgePolicy) error { + + ourChans[info.ChannelID] = true + + return cb(info, policy, policy2) + }) + if err != nil { + return err + } + + return g.remote.ForEachChannel(ctx, func(info *models.ChannelEdgeInfo, + policy *models.ChannelEdgePolicy, + policy2 *models.ChannelEdgePolicy) error { + + if ourChans[info.ChannelID] { + return nil + } + + return cb(info, policy, policy2) + }) +} + +// HasLightningNode determines if the graph has a vertex identified by the +// target node identity public key. If the node exists in the database, a +// timestamp of when the data for the node was lasted updated is returned along +// with a true boolean. Otherwise, an empty time.Time is returned with a false +// boolean. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) HasLightningNode(ctx context.Context, + nodePub [33]byte) (time.Time, bool, error) { + + timeStamp, localHas, err := g.local.HasLightningNode(ctx, nodePub) + if err != nil { + return timeStamp, false, err + } + if localHas { + return timeStamp, true, nil + } + + return g.remote.HasLightningNode(ctx, nodePub) +} + +// LookupAlias attempts to return the alias as advertised by the target node. +// graphdb.ErrNodeAliasNotFound is returned if the alias is not found. +// +// NOTE: this is part of the GraphSource interface. +func (g *GraphSourceMux) LookupAlias(ctx context.Context, + pub *btcec.PublicKey) (string, error) { + + // First check locally. + alias, err := g.local.LookupAlias(ctx, pub) + if err == nil { + return alias, nil + } + if !errors.Is(err, graphdb.ErrNodeAliasNotFound) { + return "", err + } + + return g.remote.LookupAlias(ctx, pub) +} + +// selfNodePub fetches the local nodes pub key. It first checks the cached value +// and if non exists, it queries the database. +func (g *GraphSourceMux) selfNodePub() (route.Vertex, error) { + g.mu.Lock() + defer g.mu.Unlock() + + if g.srcPub != nil { + return *g.srcPub, nil + } + + pub, err := g.local.SelfNode() + if err != nil { + return route.Vertex{}, err + } + + g.srcPub = &pub + + return *g.srcPub, nil +} + +type rTxConstructor interface { + NewPathFindTx(ctx context.Context) (graphsession.RTx, error) +} + +// rTxSet is an implementation of graphdb.RTx which is backed a read transaction +// for the local graph and one for a remote graph. +type rTxSet struct { + lRTx graphsession.RTx + rRTx graphsession.RTx +} + +// newMultiRTx uses the given rTxConstructors to begin a read transaction for +// each and returns a multiRTx that represents this open set of transactions. +func newRTxSet(ctx context.Context, localConstructor, + remoteConstructor rTxConstructor) (*rTxSet, error) { + + localRTx, err := localConstructor.NewPathFindTx(ctx) + if err != nil { + return nil, err + } + + remoteRTx, err := remoteConstructor.NewPathFindTx(ctx) + if err != nil { + _ = localRTx.Close() + + return nil, err + } + + return &rTxSet{ + lRTx: localRTx, + rRTx: remoteRTx, + }, nil +} + +// Close closes all the transactions held by multiRTx. +// +// NOTE: this is part of the graphdb.RTx interface. +func (s *rTxSet) Close() error { + var returnErr error + + if s.lRTx != nil { + if err := s.lRTx.Close(); err != nil { + returnErr = err + } + } + + if s.rRTx != nil { + if err := s.rRTx.Close(); err != nil { + returnErr = err + } + } + + return returnErr +} + +// MustImplementRTx is a helper method that ensures that the rTxSet type +// implements the RTx interface. +// +// NOTE: this is part of the graphdb.RTx interface. +func (s *rTxSet) MustImplementRTx() {} + +// A compile-time check to ensure that multiRTx implements graphdb.RTx. +var _ graphsession.RTx = (*rTxSet)(nil) + +// extractRTxSet is a helper function that casts an RTx into a rTxSet returns +// the local and remote RTxs respectively. +func extractRTxSet(tx graphsession.RTx) (graphsession.RTx, graphsession.RTx, + error) { + + if tx == nil { + return nil, nil, nil + } + + set, ok := tx.(*rTxSet) + if !ok { + return nil, nil, fmt.Errorf("expected a rTxSet, got %T", tx) + } + + return set.lRTx, set.rRTx, nil +} diff --git a/config.go b/config.go index a1c8e3c616f..6e5b0ca5026 100644 --- a/config.go +++ b/config.go @@ -856,6 +856,87 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) { return cleanCfg, nil } +func LoadConfigNoFlags(preCfg Config, interceptor signal.Interceptor) (*Config, error) { + // Show the version and exit if the version flag was specified. + appName := filepath.Base(os.Args[0]) + appName = strings.TrimSuffix(appName, filepath.Ext(appName)) + usageMessage := fmt.Sprintf("Use %s -h to show usage", appName) + if preCfg.ShowVersion { + fmt.Println(appName, "version", build.Version(), + "commit="+build.Commit) + os.Exit(0) + } + // If the config file path has not been modified by the user, then we'll + // use the default config file path. However, if the user has modified + // their lnddir, then we should assume they intend to use the config + // file within it. + configFileDir := CleanAndExpandPath(preCfg.LndDir) + configFilePath := CleanAndExpandPath(preCfg.ConfigFile) + switch { + // User specified --lnddir but no --configfile. Update the config file + // path to the lnd config directory, but don't require it to exist. + case configFileDir != DefaultLndDir && + configFilePath == DefaultConfigFile: + configFilePath = filepath.Join( + configFileDir, lncfg.DefaultConfigFilename, + ) + // User did specify an explicit --configfile, so we check that it does + // exist under that path to avoid surprises. + case configFilePath != DefaultConfigFile: + if !lnrpc.FileExists(configFilePath) { + return nil, fmt.Errorf("specified config file does "+ + "not exist in %s", configFilePath) + } + } + // Next, load any additional configuration options from the file. + var configFileError error + cfg := preCfg + fileParser := flags.NewParser(&cfg, flags.Default) + err := flags.NewIniParser(fileParser).ParseFile(configFilePath) + if err != nil { + // If it's a parsing related error, then we'll return + // immediately, otherwise we can proceed as possibly the config + // file doesn't exist which is OK. + if lnutils.ErrorAs[*flags.IniError](err) || + lnutils.ErrorAs[*flags.Error](err) { + return nil, err + } + configFileError = err + } + // Make sure everything we just loaded makes sense. + cleanCfg, err := ValidateConfig( + cfg, interceptor, fileParser, nil, + ) + var usageErr *lncfg.UsageError + if errors.As(err, &usageErr) { + // The logging system might not yet be initialized, so we also + // write to stderr to make sure the error appears somewhere. + _, _ = fmt.Fprintln(os.Stderr, usageMessage) + ltndLog.Warnf("Incorrect usage: %v", usageMessage) + // The log subsystem might not yet be initialized. But we still + // try to log the error there since some packaging solutions + // might only look at the log and not stdout/stderr. + ltndLog.Warnf("Error validating config: %v", err) + return nil, err + } + if err != nil { + // The log subsystem might not yet be initialized. But we still + // try to log the error there since some packaging solutions + // might only look at the log and not stdout/stderr. + ltndLog.Warnf("Error validating config: %v", err) + return nil, err + } + // Warn about missing config file only after all other configuration is + // done. This prevents the warning on help messages and invalid options. + // Note this should go directly before the return. + if configFileError != nil { + ltndLog.Warnf("%v", configFileError) + } + // Finally, log warnings for deprecated config options if they are set. + logWarningsForDeprecation(*cleanCfg) + return cleanCfg, nil +} + // ValidateConfig check the given configuration to be sane. This makes sure no // illegal values or combination of values are set. All file system paths are // normalized. The cleaned up config is returned on success. @@ -936,11 +1017,18 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser, fileOptionNested := fileParser.FindOptionByLongName( "lnd." + long, ) - flagOption := flagParser.FindOptionByLongName(long) - flagOptionNested := flagParser.FindOptionByLongName( - "lnd." + long, + var ( + flagOption *flags.Option + flagOptionNested *flags.Option ) + if flagParser != nil { + flagOption = flagParser.FindOptionByLongName(long) + flagOptionNested = flagParser.FindOptionByLongName( + "lnd." + long, + ) + } + return (fileOption != nil && fileOption.IsSet()) || (fileOptionNested != nil && fileOptionNested.IsSet()) || (flagOption != nil && flagOption.IsSet()) || diff --git a/graph/sources/chan_graph.go b/graph/sources/chan_graph.go index 6e1f0080ba5..2b481a71904 100644 --- a/graph/sources/chan_graph.go +++ b/graph/sources/chan_graph.go @@ -456,3 +456,12 @@ func extractKVDBRTx(tx session.RTx) (kvdb.RTx, error) { return kvdbTx, nil } + +func (s *DBSource) SelfNode() (route.Vertex, error) { + node, err := s.db.SourceNode() + if err != nil { + return route.Vertex{}, err + } + + return route.NewVertexFromBytes(node.PubKeyBytes[:]) +}