Skip to content

Commit f8399e7

Browse files
committed
graph: check zombie chan by age
In this commit, we add a method to determine if a channel is a zombie using the timestamp of the block of the funding trx that opened the channel.
1 parent f0e360f commit f8399e7

File tree

2 files changed

+115
-0
lines changed

2 files changed

+115
-0
lines changed

graph/builder.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,26 @@ func (b *Builder) IsZombieChannel(updateTime1,
499499
return e1Zombie && e2Zombie
500500
}
501501

502+
// IsZombieByAge checks if a channel is a zombie by its age. It uses the
503+
// timestamp of the block of the transaction that opened the channel. We use
504+
// this only for channels that have no edge policies, as we can't use the last
505+
// update timestamp to determine if the channel is a zombie.
506+
func (b *Builder) IsZombieByAge(scid uint64) (bool, error) {
507+
blockHeight := lnwire.NewShortChanIDFromInt(scid).BlockHeight
508+
509+
blockhash, err := b.cfg.Chain.GetBlockHash(int64(blockHeight))
510+
if err != nil {
511+
return false, err
512+
}
513+
514+
header, err := b.cfg.Chain.GetBlockHeader(blockhash)
515+
if err != nil {
516+
return false, err
517+
}
518+
519+
return time.Since(header.Timestamp) >= b.cfg.ChannelPruneExpiry, nil
520+
}
521+
502522
// pruneZombieChans is a method that will be called periodically to prune out
503523
// any "zombie" channels. We consider channels zombies if *both* edges haven't
504524
// been updated since our zombie horizon. If AssumeChannelValid is present,
@@ -536,6 +556,25 @@ func (b *Builder) pruneZombieChans() error {
536556
return nil
537557
}
538558

559+
// If both edges are nil, then we'll check if the channel is a
560+
// zombie that has been opened for long and never received a
561+
// policy update.
562+
if e1 == nil && e2 == nil {
563+
isZombie, err := b.IsZombieByAge(info.ChannelID)
564+
if err != nil {
565+
return fmt.Errorf("unable to check if "+
566+
"channel is a zombie: %w", err)
567+
}
568+
569+
if isZombie {
570+
chansToPrune[info.ChannelID] = struct{}{}
571+
}
572+
573+
// We've handled channels with no policies, so we can
574+
// exit early to process the next channel.
575+
return nil
576+
}
577+
539578
e1Zombie, e2Zombie, isZombieChan := b.isZombieChannel(e1, e2)
540579

541580
if e1Zombie {

graph/builder_test.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -884,6 +884,82 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
884884
}
885885
}
886886

887+
// TestIsZombieByAge tests that we can properly determine if a channel with no
888+
// edge policies is a zombie or not using the block timestamp that the
889+
// transaction that opened the channel was included in.
890+
//
891+
//nolint:ll
892+
func TestIsZombieByAge(t *testing.T) {
893+
t.Parallel()
894+
895+
tests := []struct {
896+
name string
897+
blockTimestamp time.Time
898+
channelPruneExpiry time.Duration
899+
expectedPrune bool
900+
}{
901+
{
902+
name: "old chan",
903+
blockTimestamp: time.Now().Add(-30 * 24 * time.Hour),
904+
channelPruneExpiry: 14 * 24 * time.Hour,
905+
expectedPrune: true,
906+
},
907+
{
908+
name: "recent channel",
909+
blockTimestamp: time.Now().Add(-7 * 24 * time.Hour),
910+
channelPruneExpiry: 14 * 24 * time.Hour,
911+
expectedPrune: false,
912+
},
913+
{
914+
name: "chan at threshold",
915+
blockTimestamp: time.Now().Add(-14 * 24 * time.Hour),
916+
channelPruneExpiry: 14 * 24 * time.Hour,
917+
expectedPrune: true,
918+
},
919+
}
920+
921+
for _, tc := range tests {
922+
t.Run(tc.name, func(t *testing.T) {
923+
// Create mock chain with a starting height.
924+
const startingHeight = 100
925+
mockChain := newMockChain(startingHeight)
926+
927+
// Create a block with the desired timestamp.
928+
block := &wire.MsgBlock{
929+
Header: wire.BlockHeader{
930+
Timestamp: tc.blockTimestamp,
931+
},
932+
}
933+
934+
// Add the block at a specific height.
935+
const channelBlockHeight = 101
936+
mockChain.addBlock(block, channelBlockHeight, 0)
937+
938+
scid := lnwire.ShortChannelID{
939+
BlockHeight: channelBlockHeight,
940+
TxIndex: 0,
941+
TxPosition: 0,
942+
}
943+
944+
// Create a minimal builder config that consist the mock
945+
// chain and the channel prune expiry we set.
946+
cfg := &Config{
947+
Chain: mockChain,
948+
ChannelPruneExpiry: tc.channelPruneExpiry,
949+
}
950+
builder := &Builder{
951+
cfg: cfg,
952+
}
953+
954+
// Test the method to see we are able to determine if
955+
// the channel is a zombie or not.
956+
isZombie, err := builder.IsZombieByAge(scid.ToUint64())
957+
require.NoError(t, err)
958+
require.Equal(t, tc.expectedPrune, isZombie)
959+
})
960+
}
961+
}
962+
887963
// TestPruneChannelGraphDoubleDisabled test that we can properly prune channels
888964
// with both edges disabled from our channel graph.
889965
func TestPruneChannelGraphDoubleDisabled(t *testing.T) {

0 commit comments

Comments
 (0)