Skip to content

Commit 33a0cbe

Browse files
authored
Merge pull request #7292 from Crypt-iQ/disconnectblock_patch
aliasmgr+channeldb: stop deleting zero-conf edges from graph if reorg occurs
2 parents 292551f + b002879 commit 33a0cbe

File tree

6 files changed

+198
-18
lines changed

6 files changed

+198
-18
lines changed

aliasmgr/aliasmgr.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ var (
5353
// endBlockHeight is the ending block height of the alias range.
5454
endBlockHeight = 16_250_000
5555

56-
// startingAlias is the first alias ShortChannelID that will get
56+
// StartingAlias is the first alias ShortChannelID that will get
5757
// assigned by RequestAlias. The starting BlockHeight is chosen so that
5858
// legitimate SCIDs in integration tests aren't mistaken for an alias.
59-
startingAlias = lnwire.ShortChannelID{
59+
StartingAlias = lnwire.ShortChannelID{
6060
BlockHeight: uint32(startingBlockHeight),
6161
TxIndex: 0,
6262
TxPosition: 0,
@@ -401,8 +401,8 @@ func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
401401
lastBytes := bucket.Get(lastAliasKey)
402402
if lastBytes == nil {
403403
// If the key does not exist, then we can write the
404-
// startingAlias to it.
405-
nextAlias = startingAlias
404+
// StartingAlias to it.
405+
nextAlias = StartingAlias
406406

407407
var scratch [8]byte
408408
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())

aliasmgr/aliasmgr_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ func TestAliasStorePeerAlias(t *testing.T) {
3232

3333
// Test that we can put the (chanID, alias) mapping in the database.
3434
// Also check that we retrieve exactly what we put in.
35-
err = aliasStore.PutPeerAlias(chanID1, startingAlias)
35+
err = aliasStore.PutPeerAlias(chanID1, StartingAlias)
3636
require.NoError(t, err)
3737

3838
storedAlias, err := aliasStore.GetPeerAlias(chanID1)
3939
require.NoError(t, err)
40-
require.Equal(t, startingAlias, storedAlias)
40+
require.Equal(t, StartingAlias, storedAlias)
4141
}
4242

4343
// TestAliasStoreRequest tests that the aliasStore delivers the expected SCID.
@@ -55,12 +55,12 @@ func TestAliasStoreRequest(t *testing.T) {
5555
aliasStore, err := NewManager(db)
5656
require.NoError(t, err)
5757

58-
// We'll assert that the very first alias we receive is startingAlias.
58+
// We'll assert that the very first alias we receive is StartingAlias.
5959
alias1, err := aliasStore.RequestAlias()
6060
require.NoError(t, err)
61-
require.Equal(t, startingAlias, alias1)
61+
require.Equal(t, StartingAlias, alias1)
6262

63-
// The next alias should be the result of passing in startingAlias to
63+
// The next alias should be the result of passing in StartingAlias to
6464
// getNextScid.
6565
nextAlias := getNextScid(alias1)
6666
alias2, err := aliasStore.RequestAlias()
@@ -78,7 +78,7 @@ func TestGetNextScid(t *testing.T) {
7878
}{
7979
{
8080
name: "starting alias",
81-
current: startingAlias,
81+
current: StartingAlias,
8282
expected: lnwire.ShortChannelID{
8383
BlockHeight: uint32(startingBlockHeight),
8484
TxIndex: 0,

channeldb/graph.go

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
"github.com/btcsuite/btcd/chaincfg/chainhash"
2121
"github.com/btcsuite/btcd/txscript"
2222
"github.com/btcsuite/btcd/wire"
23+
"github.com/lightningnetwork/lnd/aliasmgr"
2324
"github.com/lightningnetwork/lnd/batch"
2425
"github.com/lightningnetwork/lnd/kvdb"
2526
"github.com/lightningnetwork/lnd/lnwire"
@@ -1525,12 +1526,10 @@ func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ([]*ChannelEdgeInf
15251526
BlockHeight: height,
15261527
}
15271528

1528-
// Delete everything after this height from the db.
1529-
endShortChanID := lnwire.ShortChannelID{
1530-
BlockHeight: math.MaxUint32 & 0x00ffffff,
1531-
TxIndex: math.MaxUint32 & 0x00ffffff,
1532-
TxPosition: math.MaxUint16,
1533-
}
1529+
// Delete everything after this height from the db up until the
1530+
// SCID alias range.
1531+
endShortChanID := aliasmgr.StartingAlias
1532+
15341533
// The block height will be the 3 first bytes of the channel IDs.
15351534
var chanIDStart [8]byte
15361535
byteOrder.PutUint64(chanIDStart[:], startShortChanID.ToUint64())
@@ -1569,11 +1568,14 @@ func (c *ChannelGraph) DisconnectBlockAtHeight(height uint32) ([]*ChannelEdgeInf
15691568
// found edge.
15701569
// NOTE: we must delete the edges after the cursor loop, since
15711570
// modifying the bucket while traversing is not safe.
1571+
// NOTE: We use a < comparison in bytes.Compare instead of <=
1572+
// so that the StartingAlias itself isn't deleted.
15721573
var keys [][]byte
15731574
cursor := edgeIndex.ReadWriteCursor()
1574-
for k, v := cursor.Seek(chanIDStart[:]); k != nil &&
1575-
bytes.Compare(k, chanIDEnd[:]) <= 0; k, v = cursor.Next() {
15761575

1576+
//nolint:lll
1577+
for k, v := cursor.Seek(chanIDStart[:]); k != nil &&
1578+
bytes.Compare(k, chanIDEnd[:]) < 0; k, v = cursor.Next() {
15771579
edgeInfoReader := bytes.NewReader(v)
15781580
edgeInfo, err := deserializeChanEdgeInfo(edgeInfoReader)
15791581
if err != nil {

docs/release-notes/release-notes-0.16.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,9 @@ in the lnwire package](https://github.com/lightningnetwork/lnd/pull/7303)
239239
* [Remove non-existent Cleanup calls from etcd test code in the `kvdb`
240240
package](https://github.com/lightningnetwork/lnd/pull/7352)
241241

242+
* [A bug has been fixed where a reorg would cause zero-conf channels to be deleted
243+
from the graph.](https://github.com/lightningnetwork/lnd/pull/7292)
244+
242245
## `lncli`
243246

244247
* [Add an `insecure` flag to skip tls auth as well as a `metadata` string slice

lntest/itest/list_on_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -505,4 +505,8 @@ var allTestCasesTemp = []*lntemp.TestCase{
505505
Name: "sign verify message with addr",
506506
TestFunc: testSignVerifyMessageWithAddr,
507507
},
508+
{
509+
Name: "zero conf reorg edge existence",
510+
TestFunc: testZeroConfReorg,
511+
},
508512
}

lntest/itest/lnd_zero_conf_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
package itest
22

33
import (
4+
"context"
45
"testing"
56
"time"
67

8+
"github.com/btcsuite/btcd/btcjson"
79
"github.com/btcsuite/btcd/btcutil"
10+
"github.com/btcsuite/btcd/integration/rpctest"
811
"github.com/go-errors/errors"
12+
"github.com/lightningnetwork/lnd/aliasmgr"
913
"github.com/lightningnetwork/lnd/chainreg"
1014
"github.com/lightningnetwork/lnd/lnrpc"
1115
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1216
"github.com/lightningnetwork/lnd/lntemp"
1317
"github.com/lightningnetwork/lnd/lntemp/node"
1418
"github.com/lightningnetwork/lnd/lntemp/rpc"
19+
"github.com/lightningnetwork/lnd/lntest"
1520
"github.com/lightningnetwork/lnd/lntest/wait"
1621
"github.com/lightningnetwork/lnd/lnwire"
1722
"github.com/stretchr/testify/require"
@@ -883,3 +888,169 @@ func acceptChannel(t *testing.T, zeroConf bool, stream rpc.AcceptorClient) {
883888
err = stream.Send(resp)
884889
require.NoError(t, err)
885890
}
891+
892+
// testZeroConfReorg tests that a reorg does not cause a zero-conf channel to
893+
// be deleted from the channel graph. This was previously the case due to logic
894+
// in the function DisconnectBlockAtHeight.
895+
func testZeroConfReorg(ht *lntemp.HarnessTest) {
896+
if ht.ChainBackendName() == lntest.NeutrinoBackendName {
897+
ht.Skipf("skipping zero-conf reorg test for neutrino backend")
898+
}
899+
900+
var (
901+
ctxb = context.Background()
902+
temp = "temp"
903+
)
904+
905+
// Since zero-conf is opt in, the harness nodes provided won't be able
906+
// to open zero-conf channels. In that case, we just spin up new nodes.
907+
zeroConfArgs := []string{
908+
"--protocol.option-scid-alias",
909+
"--protocol.zero-conf",
910+
"--protocol.anchors",
911+
}
912+
913+
carol := ht.NewNode("Carol", zeroConfArgs)
914+
915+
// Spin-up Dave so Carol can open a zero-conf channel to him.
916+
dave := ht.NewNode("Dave", zeroConfArgs)
917+
918+
// We'll give Carol some coins in order to fund the channel.
919+
ht.FundCoins(btcutil.SatoshiPerBitcoin, carol)
920+
921+
// Ensure that both Carol and Dave are connected.
922+
ht.EnsureConnected(carol, dave)
923+
924+
// Setup a ChannelAcceptor for Dave.
925+
acceptStream, cancel := dave.RPC.ChannelAcceptor()
926+
go acceptChannel(ht.T, true, acceptStream)
927+
928+
// Open a private zero-conf anchors channel of 1M satoshis.
929+
params := lntemp.OpenChannelParams{
930+
Amt: btcutil.Amount(1_000_000),
931+
CommitmentType: lnrpc.CommitmentType_ANCHORS,
932+
ZeroConf: true,
933+
}
934+
_ = ht.OpenChannelNoAnnounce(carol, dave, params)
935+
936+
// Remove the ChannelAcceptor.
937+
cancel()
938+
939+
// Attempt to send a 10K satoshi payment from Carol to Dave. This
940+
// requires that the edge exists in the graph.
941+
daveInvoiceParams := &lnrpc.Invoice{
942+
Value: int64(10_000),
943+
}
944+
daveInvoiceResp := dave.RPC.AddInvoice(daveInvoiceParams)
945+
946+
payReqs := []string{daveInvoiceResp.PaymentRequest}
947+
ht.CompletePaymentRequests(carol, payReqs)
948+
949+
// We will now attempt to query for the alias SCID in Carol's graph.
950+
// We will query for the starting alias, which is exported by the
951+
// aliasmgr package.
952+
_, err := carol.RPC.LN.GetChanInfo(ctxb, &lnrpc.ChanInfoRequest{
953+
ChanId: aliasmgr.StartingAlias.ToUint64(),
954+
})
955+
require.NoError(ht.T, err)
956+
957+
// Now we will trigger a reorg and we'll assert that the edge still
958+
// exists in the graph.
959+
//
960+
// First, we'll setup a new miner that we can use to cause a reorg.
961+
tempLogDir := ".tempminerlogs"
962+
logFilename := "output-open_channel_reorg-temp_miner.log"
963+
tempMiner := lntemp.NewTempMiner(
964+
ht.Context(), ht.T, tempLogDir, logFilename,
965+
)
966+
defer tempMiner.Stop()
967+
968+
require.NoError(
969+
ht.T, tempMiner.SetUp(false, 0), "unable to setup mining node",
970+
)
971+
972+
// We start by connecting the new miner to our original miner, such
973+
// that it will sync to our original chain.
974+
err = ht.Miner.Client.Node(
975+
btcjson.NConnect, tempMiner.P2PAddress(), &temp,
976+
)
977+
require.NoError(ht.T, err, "unable to connect node")
978+
979+
nodeSlice := []*rpctest.Harness{ht.Miner.Harness, tempMiner.Harness}
980+
err = rpctest.JoinNodes(nodeSlice, rpctest.Blocks)
981+
require.NoError(ht.T, err, "unable to join node on blocks")
982+
983+
// The two miners should be on the same block height.
984+
assertMinerBlockHeightDelta(ht, ht.Miner, tempMiner, 0)
985+
986+
// We disconnect the two miners, such that we can mine two chains and
987+
// cause a reorg later.
988+
err = ht.Miner.Client.Node(
989+
btcjson.NDisconnect, tempMiner.P2PAddress(), &temp,
990+
)
991+
require.NoError(ht.T, err, "unable to remove node")
992+
993+
// We now cause a fork, by letting our original miner mine 1 block and
994+
// our new miner will mine 2.
995+
ht.MineBlocks(1)
996+
_, err = tempMiner.Client.Generate(2)
997+
require.NoError(ht.T, err, "unable to generate blocks")
998+
999+
// Ensure the temp miner is one block ahead.
1000+
assertMinerBlockHeightDelta(ht, ht.Miner, tempMiner, 1)
1001+
1002+
// Wait for Carol to sync to the original miner's chain.
1003+
_, minerHeight, err := ht.Miner.Client.GetBestBlock()
1004+
require.NoError(ht.T, err, "unable to get current blockheight")
1005+
1006+
ht.WaitForNodeBlockHeight(carol, minerHeight)
1007+
1008+
// Now we'll disconnect Carol's chain backend from the original miner
1009+
// so that we can connect the two miners together and let the original
1010+
// miner sync to the temp miner's chain.
1011+
ht.DisconnectMiner()
1012+
1013+
// Connecting to the temporary miner should cause the original miner to
1014+
// reorg to the longer chain.
1015+
err = ht.Miner.Client.Node(
1016+
btcjson.NConnect, tempMiner.P2PAddress(), &temp,
1017+
)
1018+
require.NoError(ht.T, err, "unable to remove node")
1019+
1020+
nodes := []*rpctest.Harness{tempMiner.Harness, ht.Miner.Harness}
1021+
err = rpctest.JoinNodes(nodes, rpctest.Blocks)
1022+
require.NoError(ht.T, err, "unable to join node on blocks")
1023+
1024+
// They should now be on the same chain.
1025+
assertMinerBlockHeightDelta(ht, ht.Miner, tempMiner, 0)
1026+
1027+
// Now we disconnect the two miners and reconnect our original chain
1028+
// backend.
1029+
err = ht.Miner.Client.Node(
1030+
btcjson.NDisconnect, tempMiner.P2PAddress(), &temp,
1031+
)
1032+
require.NoError(ht.T, err, "unable to remove node")
1033+
1034+
ht.ConnectMiner()
1035+
1036+
// This should have caused a reorg and Alice should sync to the new
1037+
// chain.
1038+
_, tempMinerHeight, err := tempMiner.Client.GetBestBlock()
1039+
require.NoError(ht.T, err, "unable to get current blockheight")
1040+
1041+
ht.WaitForNodeBlockHeight(carol, tempMinerHeight)
1042+
1043+
err = wait.Predicate(func() bool {
1044+
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
1045+
1046+
_, err = carol.RPC.LN.GetChanInfo(ctxt, &lnrpc.ChanInfoRequest{
1047+
ChanId: aliasmgr.StartingAlias.ToUint64(),
1048+
})
1049+
1050+
return err == nil
1051+
}, defaultTimeout)
1052+
require.NoError(ht.T, err, "carol doesn't have zero-conf edge")
1053+
1054+
// Mine the zero-conf funding transaction so the test doesn't fail.
1055+
ht.MineBlocks(1)
1056+
}

0 commit comments

Comments
 (0)