Merge pull request #5138 from Roasbeef/strict-zombie
channeldb+discovery: implement strict zombie pruning
This commit is contained in:
commit
8f940a5ea3
@ -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 {
|
|
||||||
e2Zombie = time.Since(e2.LastUpdate) >= chanExpiry
|
|
||||||
if e2Zombie {
|
if e2Zombie {
|
||||||
log.Tracef("Edge #2 of ChannelID(%v) last "+
|
log.Tracef("Node2 pubkey=%x of chan_id=%v is zombie",
|
||||||
"update: %v", info.ChannelID,
|
info.NodeKey2Bytes, 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
|
||||||
@ -137,6 +138,7 @@ func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
|
|||||||
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,20 +2015,22 @@ 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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for _, strictPruning := range []bool{true, false} {
|
||||||
// We'll create our test graph and router backed with these test
|
// We'll create our test graph and router backed with these test
|
||||||
// channels we've created.
|
// channels we've created.
|
||||||
testGraph, err := createTestGraphFromChannels(testChannels, "a")
|
testGraph, err := createTestGraphFromChannels(testChannels, "a")
|
||||||
@ -2022,7 +2041,7 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
|
|||||||
|
|
||||||
const startingHeight = 100
|
const startingHeight = 100
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
||||||
startingHeight, testGraph,
|
startingHeight, testGraph, strictPruning,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create test context: %v", err)
|
t.Fatalf("unable to create test context: %v", err)
|
||||||
@ -2037,8 +2056,16 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
|
|||||||
t.Fatalf("unable to prune zombie channels: %v", err)
|
t.Fatalf("unable to prune zombie channels: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
prunedChannel := testChannels[len(testChannels)-1].ChannelID
|
// We expect channels that have either both edges stale, or one edge
|
||||||
assertChannelsPruned(t, ctx.graph, testChannels, prunedChannel)
|
// 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...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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)
|
||||||
|
Loading…
Reference in New Issue
Block a user