From 80196eb20f9c099167abb1df83254fcaf6994ae3 Mon Sep 17 00:00:00 2001 From: Wilmer Paulino Date: Wed, 17 Oct 2018 15:49:14 -0700 Subject: [PATCH] discovery: ensure we only broadcast NodeAnnouncements of public nodes In this commit, we modify the gossiper to no longer broadcast NodeAnnouncements of nodes who intend to remain private. We do this to prevent leaking their information to the greater network. --- discovery/gossiper.go | 27 +++++-- discovery/gossiper_test.go | 152 ++++++++++++++++++++++++++++++++++--- 2 files changed, 160 insertions(+), 19 deletions(-) diff --git a/discovery/gossiper.go b/discovery/gossiper.go index 641f4c9e..88fd401a 100644 --- a/discovery/gossiper.go +++ b/discovery/gossiper.go @@ -1613,13 +1613,26 @@ func (d *AuthenticatedGossiper) processNetworkAnnouncement( return nil } - // Node announcement was successfully proceeded and know it - // might be broadcast to other connected nodes. - announcements = append(announcements, networkMsg{ - peer: nMsg.peer, - source: nMsg.source, - msg: msg, - }) + // In order to ensure we don't leak unadvertised nodes, we'll + // make a quick check to ensure this node intends to publicly + // advertise itself to the network. + isPublic, err := d.cfg.Router.IsPublicNode(node.PubKeyBytes) + if err != nil { + log.Errorf("Unable to determine if node %x is "+ + "advertised: %v", node.PubKeyBytes, err) + nMsg.err <- err + return nil + } + + // If it does, we'll add their announcement to our batch so that + // it can be broadcast to the rest of our peers. + if isPublic { + announcements = append(announcements, networkMsg{ + peer: nMsg.peer, + source: nMsg.source, + msg: msg, + }) + } nMsg.err <- nil // TODO(roasbeef): get rid of the above diff --git a/discovery/gossiper_test.go b/discovery/gossiper_test.go index 56012525..56d6bdfd 100644 --- a/discovery/gossiper_test.go +++ b/discovery/gossiper_test.go @@ -1,23 +1,19 @@ package discovery import ( + "bytes" "encoding/hex" "fmt" + "io/ioutil" + "math/big" + prand "math/rand" "net" + "os" "reflect" "sync" - - prand "math/rand" - "testing" - - "math/big" - "time" - "io/ioutil" - "os" - "github.com/btcsuite/btcd/btcec" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" @@ -213,6 +209,22 @@ func (r *mockGraphSource) IsStaleNode(nodePub routing.Vertex, timestamp time.Tim return false } +// IsPublicNode determines whether the given vertex is seen as a public node in +// the graph from the graph's source node's point of view. +func (r *mockGraphSource) IsPublicNode(node routing.Vertex) (bool, error) { + for _, info := range r.infos { + if !bytes.Equal(node[:], info.NodeKey1Bytes[:]) && + !bytes.Equal(node[:], info.NodeKey2Bytes[:]) { + continue + } + + if info.AuthProof != nil { + return true, nil + } + } + return false, nil +} + // IsKnownEdge returns true if the graph source already knows of the passed // channel ID. func (r *mockGraphSource) IsKnownEdge(chanID lnwire.ShortChannelID) bool { @@ -441,10 +453,9 @@ func createUpdateAnnouncement(blockHeight uint32, flags lnwire.ChanUpdateFlag, return a, nil } -func createRemoteChannelAnnouncement(blockHeight uint32, - extraBytes ...[]byte) (*lnwire.ChannelAnnouncement, error) { +func createAnnouncementWithoutProof(blockHeight uint32, + extraBytes ...[]byte) *lnwire.ChannelAnnouncement { - var err error a := &lnwire.ChannelAnnouncement{ ShortChannelID: lnwire.ShortChannelID{ BlockHeight: blockHeight, @@ -461,6 +472,14 @@ func createRemoteChannelAnnouncement(blockHeight uint32, a.ExtraOpaqueData = extraBytes[0] } + return a +} + +func createRemoteChannelAnnouncement(blockHeight uint32, + extraBytes ...[]byte) (*lnwire.ChannelAnnouncement, error) { + + a := createAnnouncementWithoutProof(blockHeight, extraBytes...) + pub := nodeKeyPriv1.PubKey() signer := mockSigner{nodeKeyPriv1} sig, err := SignAnnouncement(&signer, pub, a) @@ -1966,6 +1985,115 @@ func TestDeDuplicatedAnnouncements(t *testing.T) { } } +// TestForwardPrivateNodeAnnouncement ensures that we do not forward node +// announcements for nodes who do not intend to publicly advertise themselves. +func TestForwardPrivateNodeAnnouncement(t *testing.T) { + t.Parallel() + + const ( + startingHeight = 100 + timestamp = 123456 + ) + + ctx, cleanup, err := createTestCtx(startingHeight) + if err != nil { + t.Fatalf("can't create context: %v", err) + } + defer cleanup() + + // We'll start off by processing a channel announcement without a proof + // (i.e., an unadvertised channel), followed by a node announcement for + // this same channel announcement. + chanAnn := createAnnouncementWithoutProof(startingHeight - 2) + pubKey := nodeKeyPriv1.PubKey() + + select { + case err := <-ctx.gossiper.ProcessLocalAnnouncement(chanAnn, pubKey): + if err != nil { + t.Fatalf("unable to process local announcement: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatalf("local announcement not processed") + } + + // The gossiper should not broadcast the announcement due to it not + // having its announcement signatures. + select { + case <-ctx.broadcastedMessage: + t.Fatal("gossiper should not have broadcast channel announcement") + case <-time.After(2 * trickleDelay): + } + + nodeAnn, err := createNodeAnnouncement(nodeKeyPriv1, timestamp) + if err != nil { + t.Fatalf("unable to create node announcement: %v", err) + } + + select { + case err := <-ctx.gossiper.ProcessLocalAnnouncement(nodeAnn, pubKey): + if err != nil { + t.Fatalf("unable to process remote announcement: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("remote announcement not processed") + } + + // The gossiper should also not broadcast the node announcement due to + // it not being part of any advertised channels. + select { + case <-ctx.broadcastedMessage: + t.Fatal("gossiper should not have broadcast node announcement") + case <-time.After(2 * trickleDelay): + } + + // Now, we'll attempt to forward the NodeAnnouncement for the same node + // by opening a public channel on the network. We'll create a + // ChannelAnnouncement and hand it off to the gossiper in order to + // process it. + remoteChanAnn, err := createRemoteChannelAnnouncement(startingHeight - 1) + if err != nil { + t.Fatalf("unable to create remote channel announcement: %v", err) + } + peer := &mockPeer{pubKey, nil, nil} + + select { + case err := <-ctx.gossiper.ProcessRemoteAnnouncement(remoteChanAnn, peer): + if err != nil { + t.Fatalf("unable to process remote announcement: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("remote announcement not processed") + } + + select { + case <-ctx.broadcastedMessage: + case <-time.After(2 * trickleDelay): + t.Fatal("gossiper should have broadcast the channel announcement") + } + + // We'll recreate the NodeAnnouncement with an updated timestamp to + // prevent a stale update. The NodeAnnouncement should now be forwarded. + nodeAnn, err = createNodeAnnouncement(nodeKeyPriv1, timestamp+1) + if err != nil { + t.Fatalf("unable to create node announcement: %v", err) + } + + select { + case err := <-ctx.gossiper.ProcessRemoteAnnouncement(nodeAnn, peer): + if err != nil { + t.Fatalf("unable to process remote announcement: %v", err) + } + case <-time.After(2 * time.Second): + t.Fatal("remote announcement not processed") + } + + select { + case <-ctx.broadcastedMessage: + case <-time.After(2 * trickleDelay): + t.Fatal("gossiper should have broadcast the node announcement") + } +} + // TestReceiveRemoteChannelUpdateFirst tests that if we receive a // ChannelUpdate from the remote before we have processed our // own ChannelAnnouncement, it will be reprocessed later, after