Merge pull request #5138 from Roasbeef/strict-zombie

channeldb+discovery: implement strict zombie pruning
This commit is contained in:
Olaoluwa Osuntokun 2021-04-21 14:03:50 -07:00 committed by GitHub
commit 8f940a5ea3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 278 additions and 131 deletions

@ -188,6 +188,7 @@ type ChannelGraph struct {
// returned instance has its own unique reject cache and channel cache. // returned instance has its own unique reject cache and channel cache.
func newChannelGraph(db *DB, rejectCacheSize, chanCacheSize int, func newChannelGraph(db *DB, rejectCacheSize, chanCacheSize int,
batchCommitInterval time.Duration) *ChannelGraph { batchCommitInterval time.Duration) *ChannelGraph {
g := &ChannelGraph{ g := &ChannelGraph{
db: db, db: db,
rejectCache: newRejectCache(rejectCacheSize), rejectCache: newRejectCache(rejectCacheSize),
@ -199,6 +200,7 @@ func newChannelGraph(db *DB, rejectCacheSize, chanCacheSize int,
g.nodeScheduler = batch.NewTimeScheduler( g.nodeScheduler = batch.NewTimeScheduler(
db.Backend, nil, batchCommitInterval, db.Backend, nil, batchCommitInterval,
) )
return g return g
} }
@ -953,7 +955,7 @@ func (c *ChannelGraph) PruneGraph(spentOutputs []*wire.OutPoint,
// was successfully pruned. // was successfully pruned.
err = delChannelEdge( err = delChannelEdge(
edges, edgeIndex, chanIndex, zombieIndex, nodes, edges, edgeIndex, chanIndex, zombieIndex, nodes,
chanID, false, chanID, false, false,
) )
if err != nil && err != ErrEdgeNotFound { if err != nil && err != ErrEdgeNotFound {
return err return err
@ -1202,7 +1204,7 @@ func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ([]*ChannelEdgeInf
for _, k := range keys { for _, k := range keys {
err = delChannelEdge( err = delChannelEdge(
edges, edgeIndex, chanIndex, zombieIndex, nodes, edges, edgeIndex, chanIndex, zombieIndex, nodes,
k, false, k, false, false,
) )
if err != nil && err != ErrEdgeNotFound { if err != nil && err != ErrEdgeNotFound {
return err return err
@ -1301,11 +1303,14 @@ func (c *ChannelGraph) PruneTip() (*chainhash.Hash, uint32, error) {
return &tipHash, tipHeight, nil return &tipHash, tipHeight, nil
} }
// DeleteChannelEdges removes edges with the given channel IDs from the database // DeleteChannelEdges removes edges with the given channel IDs from the
// and marks them as zombies. This ensures that we're unable to re-add it to our // database and marks them as zombies. This ensures that we're unable to re-add
// database once again. If an edge does not exist within the database, then // it to our database once again. If an edge does not exist within the
// ErrEdgeNotFound will be returned. // database, then ErrEdgeNotFound will be returned. If strictZombiePruning is
func (c *ChannelGraph) DeleteChannelEdges(chanIDs ...uint64) error { // true, then when we mark these edges as zombies, we'll set up the keys such
// that we require the node that failed to send the fresh update to be the one
// that resurrects the channel from its zombie state.
func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning bool, chanIDs ...uint64) error {
// TODO(roasbeef): possibly delete from node bucket if node has no more // TODO(roasbeef): possibly delete from node bucket if node has no more
// channels // channels
// TODO(roasbeef): don't delete both edges? // TODO(roasbeef): don't delete both edges?
@ -1340,7 +1345,7 @@ func (c *ChannelGraph) DeleteChannelEdges(chanIDs ...uint64) error {
byteOrder.PutUint64(rawChanID[:], chanID) byteOrder.PutUint64(rawChanID[:], chanID)
err := delChannelEdge( err := delChannelEdge(
edges, edgeIndex, chanIndex, zombieIndex, nodes, edges, edgeIndex, chanIndex, zombieIndex, nodes,
rawChanID[:], true, rawChanID[:], true, strictZombiePruning,
) )
if err != nil { if err != nil {
return err return err
@ -1929,7 +1934,7 @@ func delEdgeUpdateIndexEntry(edgesBucket kvdb.RwBucket, chanID uint64,
} }
func delChannelEdge(edges, edgeIndex, chanIndex, zombieIndex, func delChannelEdge(edges, edgeIndex, chanIndex, zombieIndex,
nodes kvdb.RwBucket, chanID []byte, isZombie bool) error { nodes kvdb.RwBucket, chanID []byte, isZombie, strictZombie bool) error {
edgeInfo, err := fetchChanEdgeInfo(edgeIndex, chanID) edgeInfo, err := fetchChanEdgeInfo(edgeIndex, chanID)
if err != nil { if err != nil {
@ -1997,12 +2002,57 @@ func delChannelEdge(edges, edgeIndex, chanIndex, zombieIndex,
return nil return nil
} }
nodeKey1, nodeKey2 := edgeInfo.NodeKey1Bytes, edgeInfo.NodeKey2Bytes
if strictZombie {
nodeKey1, nodeKey2 = makeZombiePubkeys(&edgeInfo, edge1, edge2)
}
return markEdgeZombie( return markEdgeZombie(
zombieIndex, byteOrder.Uint64(chanID), edgeInfo.NodeKey1Bytes, zombieIndex, byteOrder.Uint64(chanID), nodeKey1, nodeKey2,
edgeInfo.NodeKey2Bytes,
) )
} }
// makeZombiePubkeys derives the node pubkeys to store in the zombie index for a
// particular pair of channel policies. The return values are one of:
// 1. (pubkey1, pubkey2)
// 2. (pubkey1, blank)
// 3. (blank, pubkey2)
//
// A blank pubkey means that corresponding node will be unable to resurrect a
// channel on its own. For example, node1 may continue to publish recent
// updates, but node2 has fallen way behind. After marking an edge as a zombie,
// we don't want another fresh update from node1 to resurrect, as the edge can
// only become live once node2 finally sends something recent.
//
// In the case where we have neither update, we allow either party to resurrect
// the channel. If the channel were to be marked zombie again, it would be
// marked with the correct lagging channel since we received an update from only
// one side.
func makeZombiePubkeys(info *ChannelEdgeInfo,
e1, e2 *ChannelEdgePolicy) ([33]byte, [33]byte) {
switch {
// If we don't have either edge policy, we'll return both pubkeys so
// that the channel can be resurrected by either party.
case e1 == nil && e2 == nil:
return info.NodeKey1Bytes, info.NodeKey2Bytes
// If we're missing edge1, or if both edges are present but edge1 is
// older, we'll return edge1's pubkey and a blank pubkey for edge2. This
// means that only an update from edge1 will be able to resurrect the
// channel.
case e1 == nil || (e2 != nil && e1.LastUpdate.Before(e2.LastUpdate)):
return info.NodeKey1Bytes, [33]byte{}
// Otherwise, we're missing edge2 or edge2 is the older side, so we
// return a blank pubkey for edge1. In this case, only an update from
// edge2 can resurect the channel.
default:
return [33]byte{}, info.NodeKey2Bytes
}
}
// UpdateEdgePolicy updates the edge routing policy for a single directed edge // UpdateEdgePolicy updates the edge routing policy for a single directed edge
// within the database for the referenced channel. The `flags` attribute within // within the database for the referenced channel. The `flags` attribute within
// the ChannelEdgePolicy determines which of the directed edges are being // the ChannelEdgePolicy determines which of the directed edges are being

@ -368,7 +368,7 @@ func TestEdgeInsertionDeletion(t *testing.T) {
// Next, attempt to delete the edge from the database, again this // Next, attempt to delete the edge from the database, again this
// should proceed without any issues. // should proceed without any issues.
if err := graph.DeleteChannelEdges(chanID); err != nil { if err := graph.DeleteChannelEdges(false, chanID); err != nil {
t.Fatalf("unable to delete edge: %v", err) t.Fatalf("unable to delete edge: %v", err)
} }
@ -387,7 +387,7 @@ func TestEdgeInsertionDeletion(t *testing.T) {
// Finally, attempt to delete a (now) non-existent edge within the // Finally, attempt to delete a (now) non-existent edge within the
// database, this should result in an error. // database, this should result in an error.
err = graph.DeleteChannelEdges(chanID) err = graph.DeleteChannelEdges(false, chanID)
if err != ErrEdgeNotFound { if err != ErrEdgeNotFound {
t.Fatalf("deleting a non-existent edge should fail!") t.Fatalf("deleting a non-existent edge should fail!")
} }
@ -1756,7 +1756,7 @@ func TestFilterKnownChanIDs(t *testing.T) {
if err := graph.AddChannelEdge(&channel); err != nil { if err := graph.AddChannelEdge(&channel); err != nil {
t.Fatalf("unable to create channel edge: %v", err) t.Fatalf("unable to create channel edge: %v", err)
} }
err := graph.DeleteChannelEdges(channel.ChannelID) err := graph.DeleteChannelEdges(false, channel.ChannelID)
if err != nil { if err != nil {
t.Fatalf("unable to mark edge zombie: %v", err) t.Fatalf("unable to mark edge zombie: %v", err)
} }
@ -2038,7 +2038,7 @@ func TestFetchChanInfos(t *testing.T) {
if err := graph.AddChannelEdge(&zombieChan); err != nil { if err := graph.AddChannelEdge(&zombieChan); err != nil {
t.Fatalf("unable to create channel edge: %v", err) t.Fatalf("unable to create channel edge: %v", err)
} }
err = graph.DeleteChannelEdges(zombieChan.ChannelID) err = graph.DeleteChannelEdges(false, zombieChan.ChannelID)
if err != nil { if err != nil {
t.Fatalf("unable to delete and mark edge zombie: %v", err) t.Fatalf("unable to delete and mark edge zombie: %v", err)
} }
@ -2654,7 +2654,7 @@ func TestNodeIsPublic(t *testing.T) {
// graph. This will make Alice be seen as a private node as it no longer // graph. This will make Alice be seen as a private node as it no longer
// has any advertised edges. // has any advertised edges.
for _, graph := range graphs { for _, graph := range graphs {
err := graph.DeleteChannelEdges(aliceBobEdge.ChannelID) err := graph.DeleteChannelEdges(false, aliceBobEdge.ChannelID)
if err != nil { if err != nil {
t.Fatalf("unable to remove edge: %v", err) t.Fatalf("unable to remove edge: %v", err)
} }
@ -2671,7 +2671,7 @@ func TestNodeIsPublic(t *testing.T) {
// completely remove the edge as it is not possible for her to know of // completely remove the edge as it is not possible for her to know of
// it without it being advertised. // it without it being advertised.
for i, graph := range graphs { for i, graph := range graphs {
err := graph.DeleteChannelEdges(bobCarolEdge.ChannelID) err := graph.DeleteChannelEdges(false, bobCarolEdge.ChannelID)
if err != nil { if err != nil {
t.Fatalf("unable to remove edge: %v", err) t.Fatalf("unable to remove edge: %v", err)
} }
@ -2779,7 +2779,7 @@ func TestDisabledChannelIDs(t *testing.T) {
} }
// Delete the channel edge and ensure it is removed from the disabled list. // Delete the channel edge and ensure it is removed from the disabled list.
if err = graph.DeleteChannelEdges(edgeInfo.ChannelID); err != nil { if err = graph.DeleteChannelEdges(false, edgeInfo.ChannelID); err != nil {
t.Fatalf("unable to delete channel edge: %v", err) t.Fatalf("unable to delete channel edge: %v", err)
} }
disabledChanIds, err = graph.DisabledChannelIDs() disabledChanIds, err = graph.DisabledChannelIDs()
@ -3017,7 +3017,7 @@ func TestGraphZombieIndex(t *testing.T) {
// If we delete the edge and mark it as a zombie, then we should expect // If we delete the edge and mark it as a zombie, then we should expect
// to see it within the index. // to see it within the index.
err = graph.DeleteChannelEdges(edge.ChannelID) err = graph.DeleteChannelEdges(false, edge.ChannelID)
if err != nil { if err != nil {
t.Fatalf("unable to mark edge as zombie: %v", err) t.Fatalf("unable to mark edge as zombie: %v", err)
} }

@ -47,6 +47,10 @@ var (
// gossip syncer corresponding to a gossip query message received from // gossip syncer corresponding to a gossip query message received from
// the remote peer. // the remote peer.
ErrGossipSyncerNotFound = errors.New("gossip syncer not found") ErrGossipSyncerNotFound = errors.New("gossip syncer not found")
// emptyPubkey is used to compare compressed pubkeys against an empty
// byte array.
emptyPubkey [33]byte
) )
// optionalMsgFields is a set of optional message fields that external callers // optionalMsgFields is a set of optional message fields that external callers
@ -1881,44 +1885,13 @@ func (d *AuthenticatedGossiper) processNetworkAnnouncement(
break break
case channeldb.ErrZombieEdge: case channeldb.ErrZombieEdge:
// Since we've deemed the update as not stale above, err = d.processZombieUpdate(chanInfo, msg)
// before marking it live, we'll make sure it has been
// signed by the correct party. The least-significant
// bit in the flag on the channel update tells us which
// edge is being updated.
var pubKey *btcec.PublicKey
switch {
case msg.ChannelFlags&lnwire.ChanUpdateDirection == 0:
pubKey, _ = chanInfo.NodeKey1()
case msg.ChannelFlags&lnwire.ChanUpdateDirection == 1:
pubKey, _ = chanInfo.NodeKey2()
}
err := routing.VerifyChannelUpdateSignature(msg, pubKey)
if err != nil { if err != nil {
err := fmt.Errorf("unable to verify channel "+ log.Warn(err)
"update signature: %v", err)
log.Error(err)
nMsg.err <- err nMsg.err <- err
return nil, false return nil, false
} }
// With the signature valid, we'll proceed to mark the
// edge as live and wait for the channel announcement to
// come through again.
err = d.cfg.Router.MarkEdgeLive(msg.ShortChannelID)
if err != nil {
err := fmt.Errorf("unable to remove edge with "+
"chan_id=%v from zombie index: %v",
msg.ShortChannelID, err)
log.Error(err)
nMsg.err <- err
return nil, false
}
log.Debugf("Removed edge with chan_id=%v from zombie "+
"index", msg.ShortChannelID)
// We'll fallthrough to ensure we stash the update until // We'll fallthrough to ensure we stash the update until
// we receive its corresponding ChannelAnnouncement. // we receive its corresponding ChannelAnnouncement.
// This is needed to ensure the edge exists in the graph // This is needed to ensure the edge exists in the graph
@ -2447,6 +2420,54 @@ func (d *AuthenticatedGossiper) processNetworkAnnouncement(
} }
} }
// processZombieUpdate determines whether the provided channel update should
// resurrect a given zombie edge.
func (d *AuthenticatedGossiper) processZombieUpdate(
chanInfo *channeldb.ChannelEdgeInfo, msg *lnwire.ChannelUpdate) error {
// The least-significant bit in the flag on the channel update tells us
// which edge is being updated.
isNode1 := msg.ChannelFlags&lnwire.ChanUpdateDirection == 0
// Since we've deemed the update as not stale above, before marking it
// live, we'll make sure it has been signed by the correct party. If we
// have both pubkeys, either party can resurect the channel. If we've
// already marked this with the stricter, single-sided resurrection we
// will only have the pubkey of the node with the oldest timestamp.
var pubKey *btcec.PublicKey
switch {
case isNode1 && chanInfo.NodeKey1Bytes != emptyPubkey:
pubKey, _ = chanInfo.NodeKey1()
case !isNode1 && chanInfo.NodeKey2Bytes != emptyPubkey:
pubKey, _ = chanInfo.NodeKey2()
}
if pubKey == nil {
return fmt.Errorf("incorrect pubkey to resurrect zombie "+
"with chan_id=%v", msg.ShortChannelID)
}
err := routing.VerifyChannelUpdateSignature(msg, pubKey)
if err != nil {
return fmt.Errorf("unable to verify channel "+
"update signature: %v", err)
}
// With the signature valid, we'll proceed to mark the
// edge as live and wait for the channel announcement to
// come through again.
err = d.cfg.Router.MarkEdgeLive(msg.ShortChannelID)
if err != nil {
return fmt.Errorf("unable to remove edge with "+
"chan_id=%v from zombie index: %v",
msg.ShortChannelID, err)
}
log.Debugf("Removed edge with chan_id=%v from zombie "+
"index", msg.ShortChannelID)
return nil
}
// fetchNodeAnn fetches the latest signed node announcement from our point of // fetchNodeAnn fetches the latest signed node announcement from our point of
// view for the node with the given public key. // view for the node with the given public key.
func (d *AuthenticatedGossiper) fetchNodeAnn( func (d *AuthenticatedGossiper) fetchNodeAnn(

@ -397,7 +397,9 @@ func (r *mockGraphSource) MarkEdgeZombie(chanID lnwire.ShortChannelID, pubKey1,
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
r.zombies[chanID.ToUint64()] = [][33]byte{pubKey1, pubKey2} r.zombies[chanID.ToUint64()] = [][33]byte{pubKey1, pubKey2}
return nil return nil
} }
@ -2317,15 +2319,34 @@ func TestProcessZombieEdgeNowLive(t *testing.T) {
t.Fatalf("unable to sign update with new timestamp: %v", err) t.Fatalf("unable to sign update with new timestamp: %v", err)
} }
// We'll also add the edge to our zombie index. // We'll also add the edge to our zombie index, provide a blank pubkey
// for the first node as we're simulating the sitaution where the first
// ndoe is updating but the second node isn't. In this case we only
// want to allow a new update from the second node to allow the entire
// edge to be resurrected.
chanID := batch.chanAnn.ShortChannelID chanID := batch.chanAnn.ShortChannelID
err = ctx.router.MarkEdgeZombie( err = ctx.router.MarkEdgeZombie(
chanID, batch.chanAnn.NodeID1, batch.chanAnn.NodeID2, chanID, [33]byte{}, batch.chanAnn.NodeID2,
) )
if err != nil { if err != nil {
t.Fatalf("unable mark channel %v as zombie: %v", chanID, err) t.Fatalf("unable mark channel %v as zombie: %v", chanID, err)
} }
// If we send a new update but for the other direction of the channel,
// then it should still be rejected as we want a fresh update from the
// one that was considered stale.
batch.chanUpdAnn1.Timestamp = uint32(time.Now().Unix())
if err := signUpdate(remoteKeyPriv1, batch.chanUpdAnn1); err != nil {
t.Fatalf("unable to sign update with new timestamp: %v", err)
}
processAnnouncement(batch.chanUpdAnn1, true, true)
// At this point, the channel should still be consiered a zombie.
_, _, _, err = ctx.router.GetChannelByID(chanID)
if err != channeldb.ErrZombieEdge {
t.Fatalf("channel should still be a zombie")
}
// Attempting to process the current channel update should fail due to // Attempting to process the current channel update should fail due to
// its edge being considered a zombie and its timestamp not being within // its edge being considered a zombie and its timestamp not being within
// the live horizon. We should not expect an error here since it is just // the live horizon. We should not expect an error here since it is just

@ -2,5 +2,7 @@ package lncfg
// Routing holds the configuration options for routing. // Routing holds the configuration options for routing.
type Routing struct { type Routing struct {
AssumeChannelValid bool `long:"assumechanvalid" description:"DEPRECATED: This is now turned on by default for Neutrino (use neutrino.validatechannels=true to turn off) and shouldn't be used for any other backend! (default: false)"` AssumeChannelValid bool `long:"assumechanvalid" description:"Skip checking channel spentness during graph validation. This speedup comes at the risk of using an unvalidated view of the network for routing. (default: false)"`
StrictZombiePruning bool `long:"strictgraphpruning" description:"If true, then the graph will be pruned more aggressively for zombies. In practice this means that edges with a single stale edge will be considered a zombie."`
} }

@ -342,6 +342,13 @@ type Config struct {
// Clock is mockable time provider. // Clock is mockable time provider.
Clock clock.Clock Clock clock.Clock
// StrictZombiePruning determines if we attempt to prune zombie
// channels according to a stricter criteria. If true, then we'll prune
// a channel if only *one* of the edges is considered a zombie.
// Otherwise, we'll only prune the channel when both edges have a very
// dated last update.
StrictZombiePruning bool
} }
// EdgeLocator is a struct used to identify a specific edge. // EdgeLocator is a struct used to identify a specific edge.
@ -824,30 +831,39 @@ func (r *ChannelRouter) pruneZombieChans() error {
return nil return nil
} }
// If *both* edges haven't been updated for a period of // If either edge hasn't been updated for a period of
// chanExpiry, then we'll mark the channel itself as eligible // chanExpiry, then we'll mark the channel itself as eligible
// for graph pruning. // for graph pruning.
var e1Zombie, e2Zombie bool e1Zombie := e1 == nil || time.Since(e1.LastUpdate) >= chanExpiry
if e1 != nil { e2Zombie := e2 == nil || time.Since(e2.LastUpdate) >= chanExpiry
e1Zombie = time.Since(e1.LastUpdate) >= chanExpiry
if e1Zombie { if e1Zombie {
log.Tracef("Edge #1 of ChannelID(%v) last "+ log.Tracef("Node1 pubkey=%x of chan_id=%v is zombie",
"update: %v", info.ChannelID, info.NodeKey1Bytes, info.ChannelID)
e1.LastUpdate)
}
} }
if e2 != nil { if e2Zombie {
e2Zombie = time.Since(e2.LastUpdate) >= chanExpiry log.Tracef("Node2 pubkey=%x of chan_id=%v is zombie",
if e2Zombie { info.NodeKey2Bytes, info.ChannelID)
log.Tracef("Edge #2 of ChannelID(%v) last "+
"update: %v", info.ChannelID,
e2.LastUpdate)
}
} }
// If the channel is not considered zombie, we can move on to // If we're using strict zombie pruning, then a channel is only
// the next. // considered live if both edges have a recent update we know
if !e1Zombie || !e2Zombie { // of.
var channelIsLive bool
switch {
case r.cfg.StrictZombiePruning:
channelIsLive = !e1Zombie && !e2Zombie
// Otherwise, if we're using the less strict variant, then a
// channel is considered live if either of the edges have a
// recent update.
default:
channelIsLive = !e1Zombie || !e2Zombie
}
// Return early if the channel is still considered to be live
// with the current set of configuration parameters.
if channelIsLive {
return nil return nil
} }
@ -908,7 +924,8 @@ func (r *ChannelRouter) pruneZombieChans() error {
toPrune = append(toPrune, chanID) toPrune = append(toPrune, chanID)
log.Tracef("Pruning zombie channel with ChannelID(%v)", chanID) log.Tracef("Pruning zombie channel with ChannelID(%v)", chanID)
} }
if err := r.cfg.Graph.DeleteChannelEdges(toPrune...); err != nil { err = r.cfg.Graph.DeleteChannelEdges(r.cfg.StrictZombiePruning, toPrune...)
if err != nil {
return fmt.Errorf("unable to delete zombie channels: %v", err) return fmt.Errorf("unable to delete zombie channels: %v", err)
} }

@ -69,16 +69,17 @@ func (c *testCtx) RestartRouter() error {
return nil return nil
} }
func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGraphInstance) ( func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGraphInstance,
*testCtx, func(), error) { strictPruning bool) (*testCtx, func(), error) {
return createTestCtxFromGraphInstanceAssumeValid( return createTestCtxFromGraphInstanceAssumeValid(
startingHeight, graphInstance, false, startingHeight, graphInstance, false, strictPruning,
) )
} }
func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32, func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
graphInstance *testGraphInstance, assumeValid bool) (*testCtx, func(), error) { graphInstance *testGraphInstance, assumeValid bool,
strictPruning bool) (*testCtx, func(), error) {
// We'll initialize an instance of the channel router with mock // We'll initialize an instance of the channel router with mock
// versions of the chain and channel notifier. As we don't need to test // versions of the chain and channel notifier. As we don't need to test
@ -134,9 +135,10 @@ func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
next := atomic.AddUint64(&uniquePaymentID, 1) next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil return next, nil
}, },
PathFindingConfig: pathFindingConfig, PathFindingConfig: pathFindingConfig,
Clock: clock.NewTestClock(time.Unix(1, 0)), Clock: clock.NewTestClock(time.Unix(1, 0)),
AssumeChannelValid: assumeValid, AssumeChannelValid: assumeValid,
StrictZombiePruning: strictPruning,
}) })
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("unable to create router %v", err) return nil, nil, fmt.Errorf("unable to create router %v", err)
@ -187,7 +189,7 @@ func createTestCtxSingleNode(startingHeight uint32) (*testCtx, func(), error) {
cleanUp: cleanup, cleanUp: cleanup,
} }
return createTestCtxFromGraphInstance(startingHeight, graphInstance) return createTestCtxFromGraphInstance(startingHeight, graphInstance, false)
} }
func createTestCtxFromFile(startingHeight uint32, testGraph string) (*testCtx, func(), error) { func createTestCtxFromFile(startingHeight uint32, testGraph string) (*testCtx, func(), error) {
@ -198,7 +200,7 @@ func createTestCtxFromFile(startingHeight uint32, testGraph string) (*testCtx, f
return nil, nil, fmt.Errorf("unable to create test graph: %v", err) return nil, nil, fmt.Errorf("unable to create test graph: %v", err)
} }
return createTestCtxFromGraphInstance(startingHeight, graphInstance) return createTestCtxFromGraphInstance(startingHeight, graphInstance, false)
} }
// TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method // TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method
@ -365,9 +367,9 @@ func TestChannelUpdateValidation(t *testing.T) {
const startingBlockHeight = 101 const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromGraphInstance(startingBlockHeight, ctx, cleanUp, err := createTestCtxFromGraphInstance(
testGraph) startingBlockHeight, testGraph, true,
)
defer cleanUp() defer cleanUp()
if err != nil { if err != nil {
t.Fatalf("unable to create router: %v", err) t.Fatalf("unable to create router: %v", err)
@ -1028,7 +1030,7 @@ func TestIgnoreChannelEdgePolicyForUnknownChannel(t *testing.T) {
defer testGraph.cleanUp() defer testGraph.cleanUp()
ctx, cleanUp, err := createTestCtxFromGraphInstance( ctx, cleanUp, err := createTestCtxFromGraphInstance(
startingBlockHeight, testGraph, startingBlockHeight, testGraph, false,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create router: %v", err) t.Fatalf("unable to create router: %v", err)
@ -1946,8 +1948,8 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
freshTimestamp := time.Now() freshTimestamp := time.Now()
staleTimestamp := time.Unix(0, 0) staleTimestamp := time.Unix(0, 0)
// We'll create the following test graph so that only the last channel // We'll create the following test graph so that two of the channels
// is pruned. // are pruned.
testChannels := []*testChannel{ testChannels := []*testChannel{
// No edges. // No edges.
{ {
@ -1960,7 +1962,7 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
// Only one edge with a stale timestamp. // Only one edge with a stale timestamp.
{ {
Node1: &testChannelEnd{ Node1: &testChannelEnd{
Alias: "a", Alias: "d",
testChannelPolicy: &testChannelPolicy{ testChannelPolicy: &testChannelPolicy{
LastUpdate: staleTimestamp, LastUpdate: staleTimestamp,
}, },
@ -1970,6 +1972,20 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
ChannelID: 2, ChannelID: 2,
}, },
// Only one edge with a stale timestamp, but it's the source
// node so it won't get pruned.
{
Node1: &testChannelEnd{
Alias: "a",
testChannelPolicy: &testChannelPolicy{
LastUpdate: staleTimestamp,
},
},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 3,
},
// Only one edge with a fresh timestamp. // Only one edge with a fresh timestamp.
{ {
Node1: &testChannelEnd{ Node1: &testChannelEnd{
@ -1980,10 +1996,11 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
}, },
Node2: &testChannelEnd{Alias: "b"}, Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000, Capacity: 100000,
ChannelID: 3, ChannelID: 4,
}, },
// One edge fresh, one edge stale. // One edge fresh, one edge stale. This will be pruned with
// strict pruning activated.
{ {
Node1: &testChannelEnd{ Node1: &testChannelEnd{
Alias: "c", Alias: "c",
@ -1998,47 +2015,57 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
}, },
}, },
Capacity: 100000, Capacity: 100000,
ChannelID: 4, ChannelID: 5,
}, },
// Both edges fresh. // Both edges fresh.
symmetricTestChannel("g", "h", 100000, &testChannelPolicy{ symmetricTestChannel("g", "h", 100000, &testChannelPolicy{
LastUpdate: freshTimestamp, LastUpdate: freshTimestamp,
}, 5), }, 6),
// Both edges stale, only one pruned. // Both edges stale, only one pruned. This should be pruned for
// both normal and strict pruning.
symmetricTestChannel("e", "f", 100000, &testChannelPolicy{ symmetricTestChannel("e", "f", 100000, &testChannelPolicy{
LastUpdate: staleTimestamp, LastUpdate: staleTimestamp,
}, 6), }, 7),
} }
// We'll create our test graph and router backed with these test for _, strictPruning := range []bool{true, false} {
// channels we've created. // We'll create our test graph and router backed with these test
testGraph, err := createTestGraphFromChannels(testChannels, "a") // channels we've created.
if err != nil { testGraph, err := createTestGraphFromChannels(testChannels, "a")
t.Fatalf("unable to create test graph: %v", err) if err != nil {
t.Fatalf("unable to create test graph: %v", err)
}
defer testGraph.cleanUp()
const startingHeight = 100
ctx, cleanUp, err := createTestCtxFromGraphInstance(
startingHeight, testGraph, strictPruning,
)
if err != nil {
t.Fatalf("unable to create test context: %v", err)
}
defer cleanUp()
// All of the channels should exist before pruning them.
assertChannelsPruned(t, ctx.graph, testChannels)
// Proceed to prune the channels - only the last one should be pruned.
if err := ctx.router.pruneZombieChans(); err != nil {
t.Fatalf("unable to prune zombie channels: %v", err)
}
// We expect channels that have either both edges stale, or one edge
// stale with both known.
var prunedChannels []uint64
if strictPruning {
prunedChannels = []uint64{2, 5, 7}
} else {
prunedChannels = []uint64{2, 7}
}
assertChannelsPruned(t, ctx.graph, testChannels, prunedChannels...)
} }
defer testGraph.cleanUp()
const startingHeight = 100
ctx, cleanUp, err := createTestCtxFromGraphInstance(
startingHeight, testGraph,
)
if err != nil {
t.Fatalf("unable to create test context: %v", err)
}
defer cleanUp()
// All of the channels should exist before pruning them.
assertChannelsPruned(t, ctx.graph, testChannels)
// Proceed to prune the channels - only the last one should be pruned.
if err := ctx.router.pruneZombieChans(); err != nil {
t.Fatalf("unable to prune zombie channels: %v", err)
}
prunedChannel := testChannels[len(testChannels)-1].ChannelID
assertChannelsPruned(t, ctx.graph, testChannels, prunedChannel)
} }
// TestPruneChannelGraphDoubleDisabled test that we can properly prune channels // TestPruneChannelGraphDoubleDisabled test that we can properly prune channels
@ -2147,7 +2174,7 @@ func testPruneChannelGraphDoubleDisabled(t *testing.T, assumeValid bool) {
const startingHeight = 100 const startingHeight = 100
ctx, cleanUp, err := createTestCtxFromGraphInstanceAssumeValid( ctx, cleanUp, err := createTestCtxFromGraphInstanceAssumeValid(
startingHeight, testGraph, assumeValid, startingHeight, testGraph, assumeValid, false,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create test context: %v", err) t.Fatalf("unable to create test context: %v", err)
@ -2529,9 +2556,9 @@ func TestUnknownErrorSource(t *testing.T) {
const startingBlockHeight = 101 const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromGraphInstance(startingBlockHeight, ctx, cleanUp, err := createTestCtxFromGraphInstance(
testGraph) startingBlockHeight, testGraph, false,
)
defer cleanUp() defer cleanUp()
if err != nil { if err != nil {
t.Fatalf("unable to create router: %v", err) t.Fatalf("unable to create router: %v", err)
@ -2668,7 +2695,7 @@ func TestSendToRouteStructuredError(t *testing.T) {
const startingBlockHeight = 101 const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromGraphInstance( ctx, cleanUp, err := createTestCtxFromGraphInstance(
startingBlockHeight, testGraph, startingBlockHeight, testGraph, false,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create router: %v", err) t.Fatalf("unable to create router: %v", err)
@ -2904,7 +2931,7 @@ func TestSendToRouteMaxHops(t *testing.T) {
const startingBlockHeight = 101 const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromGraphInstance( ctx, cleanUp, err := createTestCtxFromGraphInstance(
startingBlockHeight, testGraph, startingBlockHeight, testGraph, false,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create router: %v", err) t.Fatalf("unable to create router: %v", err)
@ -3018,7 +3045,7 @@ func TestBuildRoute(t *testing.T) {
const startingBlockHeight = 101 const startingBlockHeight = 101
ctx, cleanUp, err := createTestCtxFromGraphInstance( ctx, cleanUp, err := createTestCtxFromGraphInstance(
startingBlockHeight, testGraph, startingBlockHeight, testGraph, false,
) )
if err != nil { if err != nil {
t.Fatalf("unable to create router: %v", err) t.Fatalf("unable to create router: %v", err)

@ -2344,7 +2344,7 @@ func abandonChanFromGraph(chanGraph *channeldb.ChannelGraph,
// If the channel ID is still in the graph, then that means the channel // If the channel ID is still in the graph, then that means the channel
// is still open, so we'll now move to purge it from the graph. // is still open, so we'll now move to purge it from the graph.
return chanGraph.DeleteChannelEdges(chanID) return chanGraph.DeleteChannelEdges(false, chanID)
} }
// AbandonChannel removes all channel state from the database except for a // AbandonChannel removes all channel state from the database except for a

@ -489,6 +489,12 @@ bitcoin.node=btcd
; other backend! ; other backend!
; --routing.assumechanvalid=true ; --routing.assumechanvalid=true
; If set to true, then we'll prune a channel if only a single edge is seen as
; being stale. This results in a more compact channel graph, and also is helpful
; for neutrino nodes as it means they'll only maintain edges where both nodes are
; seen as being live from it's PoV.
; --routing.strictgraphpruning=true
[Btcd] [Btcd]
; The base directory that contains the node's data, logs, configuration file, ; The base directory that contains the node's data, logs, configuration file,

@ -768,6 +768,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
s.controlTower = routing.NewControlTower(paymentControl) s.controlTower = routing.NewControlTower(paymentControl)
strictPruning := (cfg.Bitcoin.Node == "neutrino" ||
cfg.Routing.StrictZombiePruning)
s.chanRouter, err = routing.New(routing.Config{ s.chanRouter, err = routing.New(routing.Config{
Graph: chanGraph, Graph: chanGraph,
Chain: cc.ChainIO, Chain: cc.ChainIO,
@ -784,6 +786,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
NextPaymentID: sequencer.NextID, NextPaymentID: sequencer.NextID,
PathFindingConfig: pathFindingConfig, PathFindingConfig: pathFindingConfig,
Clock: clock.NewDefaultClock(), Clock: clock.NewDefaultClock(),
StrictZombiePruning: strictPruning,
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("can't create router: %v", err) return nil, fmt.Errorf("can't create router: %v", err)