Skip to content

Commit a315ad3

Browse files
authored
Merge pull request #10273 from ziggie1984/fix-nursery-height-hint
fix height hint Zero issue in utxonursery
2 parents 6ade31d + 7c92c88 commit a315ad3

File tree

3 files changed

+277
-1
lines changed

3 files changed

+277
-1
lines changed

contractcourt/utxonursery.go

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,71 @@ func NewUtxoNursery(cfg *NurseryConfig) *UtxoNursery {
242242
}
243243
}
244244

245+
// patchZeroHeightHint handles the edge case where a crib output has expiry=0
246+
// due to a historical bug. This should never happen in normal operation, but
247+
// we provide a fallback mechanism using the channel close height to determine
248+
// a valid height hint for the chain notifier.
249+
//
250+
// This function returns a height hint that ensures we don't miss confirmations
251+
// while avoiding the chain notifier's requirement that height hints must
252+
// be > 0.
253+
func (u *UtxoNursery) patchZeroHeightHint(baby *babyOutput,
254+
classHeight uint32) (uint32, error) {
255+
256+
if classHeight != 0 {
257+
// Normal case - return the original height.
258+
return classHeight, nil
259+
}
260+
261+
utxnLog.Warnf("Detected crib output %v with expiry=0, "+
262+
"attempting to use fallback height hint from channel "+
263+
"close summary", baby.OutPoint())
264+
265+
// Try to get the channel close height as a fallback.
266+
chanPoint := baby.OriginChanPoint()
267+
closeSummary, err := u.cfg.FetchClosedChannel(chanPoint)
268+
if err != nil {
269+
return 0, fmt.Errorf("cannot fetch close summary for "+
270+
"channel %v to determine fallback height hint: %w",
271+
chanPoint, err)
272+
}
273+
274+
heightHint := closeSummary.CloseHeight
275+
276+
// If the close height is 0, we try to use the short channel ID block
277+
// height as a fallback.
278+
if heightHint == 0 {
279+
if closeSummary.ShortChanID.BlockHeight == 0 {
280+
return 0, fmt.Errorf("cannot use fallback height " +
281+
"hint: close height is 0 and short " +
282+
"channel ID block height is 0")
283+
}
284+
285+
heightHint = closeSummary.ShortChanID.BlockHeight
286+
}
287+
288+
// At this point the height hint should normally be greater than the
289+
// conf depth since channels should have a minimum close height of the
290+
// segwit activation height and the conf depth which is a config
291+
// parameter should be in the single digit range.
292+
if heightHint <= u.cfg.ConfDepth {
293+
return 0, fmt.Errorf("cannot use fallback height hint: "+
294+
"fallback height hint %v <= confirmation depth %v",
295+
heightHint, u.cfg.ConfDepth)
296+
}
297+
298+
// Use the close height minus the confirmation depth as a conservative
299+
// height hint. This ensures we don't miss the confirmation even if it
300+
// happened around the close height.
301+
heightHint -= u.cfg.ConfDepth
302+
303+
utxnLog.Infof("Using fallback height hint %v for crib output "+
304+
"%v (channel closed at height %v, conf depth %v)", heightHint,
305+
baby.OutPoint(), closeSummary.CloseHeight, u.cfg.ConfDepth)
306+
307+
return heightHint, nil
308+
}
309+
245310
// Start launches all goroutines the UtxoNursery needs to properly carry out
246311
// its duties.
247312
func (u *UtxoNursery) Start() error {
@@ -967,7 +1032,19 @@ func (u *UtxoNursery) sweepCribOutput(classHeight uint32, baby *babyOutput) erro
9671032
return err
9681033
}
9691034

970-
return u.registerTimeoutConf(baby, classHeight)
1035+
// Determine the height hint to use for the confirmation notification.
1036+
// In the normal case, we use classHeight (which is the expiry height).
1037+
// However, due to a historical bug, some outputs were stored with
1038+
// expiry=0. For these cases, we need to use a fallback height hint
1039+
// based on the channel close height to avoid errors from the chain
1040+
// notifier which requires height hints > 0.
1041+
heightHint, err := u.patchZeroHeightHint(baby, classHeight)
1042+
if err != nil {
1043+
return fmt.Errorf("cannot determine height hint for "+
1044+
"crib output with expiry=0: %w", err)
1045+
}
1046+
1047+
return u.registerTimeoutConf(baby, heightHint)
9711048
}
9721049

9731050
// registerTimeoutConf is responsible for subscribing to confirmation

contractcourt/utxonursery_test.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"github.com/lightningnetwork/lnd/input"
2525
"github.com/lightningnetwork/lnd/lntest/mock"
2626
"github.com/lightningnetwork/lnd/lnwallet"
27+
"github.com/lightningnetwork/lnd/lnwire"
2728
"github.com/lightningnetwork/lnd/sweep"
2829
"github.com/stretchr/testify/require"
2930
)
@@ -1262,3 +1263,197 @@ func TestKidOutputDecode(t *testing.T) {
12621263
})
12631264
}
12641265
}
1266+
1267+
// TestPatchZeroHeightHint tests the patchZeroHeightHint function to ensure
1268+
// it correctly handles both normal cases and the edge case where classHeight
1269+
// is zero due to a historical bug.
1270+
func TestPatchZeroHeightHint(t *testing.T) {
1271+
t.Parallel()
1272+
1273+
tests := []struct {
1274+
name string
1275+
classHeight uint32
1276+
closeHeight uint32
1277+
confDepth uint32
1278+
shortChanID lnwire.ShortChannelID
1279+
fetchError error
1280+
expectedHeight uint32
1281+
expectError bool
1282+
errorContains string
1283+
}{
1284+
{
1285+
name: "normal case - non-zero class height",
1286+
classHeight: 100,
1287+
closeHeight: 200,
1288+
confDepth: 6,
1289+
shortChanID: lnwire.ShortChannelID{BlockHeight: 50},
1290+
expectedHeight: 100,
1291+
expectError: false,
1292+
},
1293+
{
1294+
name: "zero class height - fetch closed " +
1295+
"channel error",
1296+
classHeight: 0,
1297+
closeHeight: 100,
1298+
confDepth: 6,
1299+
shortChanID: lnwire.ShortChannelID{BlockHeight: 50},
1300+
fetchError: fmt.Errorf("channel not found"),
1301+
expectError: true,
1302+
errorContains: "cannot fetch close summary",
1303+
},
1304+
{
1305+
name: "zero class height - both close " +
1306+
"height and short chan ID = 0",
1307+
classHeight: 0,
1308+
closeHeight: 0,
1309+
confDepth: 6,
1310+
shortChanID: lnwire.ShortChannelID{BlockHeight: 0},
1311+
expectedHeight: 0,
1312+
expectError: true,
1313+
errorContains: "cannot use fallback height hint: " +
1314+
"close height is 0 and short channel " +
1315+
"ID block height is 0",
1316+
},
1317+
{
1318+
name: "zero class height - fallback height hint " +
1319+
"= conf depth",
1320+
classHeight: 0,
1321+
closeHeight: 6,
1322+
confDepth: 6,
1323+
shortChanID: lnwire.ShortChannelID{BlockHeight: 50},
1324+
expectedHeight: 0,
1325+
expectError: true,
1326+
errorContains: "fallback height hint 6 <= " +
1327+
"confirmation depth 6",
1328+
},
1329+
{
1330+
name: "zero class height - fallback height hint " +
1331+
"< conf depth",
1332+
classHeight: 0,
1333+
closeHeight: 3,
1334+
confDepth: 6,
1335+
shortChanID: lnwire.ShortChannelID{BlockHeight: 50},
1336+
expectedHeight: 0,
1337+
expectError: true,
1338+
errorContains: "fallback height hint 3 <= " +
1339+
"confirmation depth 6",
1340+
},
1341+
{
1342+
name: "zero class height - close " +
1343+
"height = 0, fallback height hint = conf depth",
1344+
classHeight: 0,
1345+
closeHeight: 0,
1346+
confDepth: 6,
1347+
shortChanID: lnwire.ShortChannelID{BlockHeight: 6},
1348+
expectError: true,
1349+
errorContains: "fallback height hint 6 <= " +
1350+
"confirmation depth 6",
1351+
},
1352+
{
1353+
name: "zero class height - close " +
1354+
"height = 0, fallback height hint < conf depth",
1355+
classHeight: 0,
1356+
closeHeight: 0,
1357+
confDepth: 6,
1358+
shortChanID: lnwire.ShortChannelID{BlockHeight: 3},
1359+
expectedHeight: 0,
1360+
expectError: true,
1361+
errorContains: "fallback height hint 3 <= " +
1362+
"confirmation depth 6",
1363+
},
1364+
{
1365+
name: "zero class height, fallback height is " +
1366+
"valid",
1367+
classHeight: 0,
1368+
closeHeight: 100,
1369+
confDepth: 6,
1370+
shortChanID: lnwire.ShortChannelID{BlockHeight: 50},
1371+
// heightHint - confDepth = 100 - 6 = 94.
1372+
expectedHeight: 94,
1373+
expectError: false,
1374+
},
1375+
{
1376+
name: "zero class height - close " +
1377+
"height = 0, fallback height is valid",
1378+
classHeight: 0,
1379+
closeHeight: 0,
1380+
confDepth: 6,
1381+
shortChanID: lnwire.ShortChannelID{BlockHeight: 50},
1382+
// heightHint - confDepth = 50 - 6 = 44.
1383+
expectedHeight: 44,
1384+
expectError: false,
1385+
},
1386+
}
1387+
1388+
for _, tc := range tests {
1389+
tc := tc
1390+
1391+
t.Run(tc.name, func(t *testing.T) {
1392+
t.Parallel()
1393+
1394+
// Create a mock baby output.
1395+
chanPoint := &wire.OutPoint{
1396+
Hash: [chainhash.HashSize]byte{
1397+
0x51, 0xb6, 0x37, 0xd8, 0xfc, 0xd2,
1398+
0xc6, 0xda, 0x48, 0x59, 0xe6, 0x96,
1399+
0x31, 0x13, 0xa1, 0x17, 0x2d, 0xe7,
1400+
0x93, 0xe4, 0xb7, 0x25, 0xb8, 0x4d,
1401+
0x1f, 0xb, 0x4c, 0xf9, 0x9e, 0xc5,
1402+
0x8c, 0xe9,
1403+
},
1404+
Index: 9,
1405+
}
1406+
1407+
baby := &babyOutput{
1408+
expiry: tc.classHeight,
1409+
kidOutput: kidOutput{
1410+
breachedOutput: breachedOutput{
1411+
outpoint: *chanPoint,
1412+
},
1413+
originChanPoint: *chanPoint,
1414+
},
1415+
}
1416+
1417+
cfg := &NurseryConfig{
1418+
ConfDepth: tc.confDepth,
1419+
FetchClosedChannel: func(
1420+
chanID *wire.OutPoint) (
1421+
*channeldb.ChannelCloseSummary,
1422+
error) {
1423+
1424+
if tc.fetchError != nil {
1425+
return nil, tc.fetchError
1426+
}
1427+
1428+
return &channeldb.ChannelCloseSummary{
1429+
CloseHeight: tc.closeHeight,
1430+
ShortChanID: tc.shortChanID,
1431+
}, nil
1432+
},
1433+
}
1434+
1435+
nursery := &UtxoNursery{
1436+
cfg: cfg,
1437+
}
1438+
1439+
resultHeight, err := nursery.patchZeroHeightHint(
1440+
baby, tc.classHeight,
1441+
)
1442+
1443+
if tc.expectError {
1444+
require.Error(t, err)
1445+
if tc.errorContains != "" {
1446+
require.Contains(
1447+
t, err.Error(),
1448+
tc.errorContains,
1449+
)
1450+
}
1451+
1452+
return
1453+
}
1454+
1455+
require.NoError(t, err)
1456+
require.Equal(t, tc.expectedHeight, resultHeight)
1457+
})
1458+
}
1459+
}

docs/release-notes/release-notes-0.20.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@
4646
sweeper where some outputs would not be resolved due to an error string
4747
mismatch.
4848

49+
- [Fixed](https://github.com/lightningnetwork/lnd/pull/10273) a case in the
50+
utxonursery (the legacy sweeper) where htlcs with a locktime of 0 would not
51+
be swept.
52+
4953
# New Features
5054

5155
* Use persisted [nodeannouncement](https://github.com/lightningnetwork/lnd/pull/8825)

0 commit comments

Comments
 (0)