Skip to content

Commit bed021f

Browse files
committed
contractcourt: patch 0 timelock baby outputs
Older LND versions had a bug which would create HTLCs with 0 locktime. The utxonursery will have problems dealing with such htlc outputs because we do not allow height hints of 0. Now we will fetch the closeSummary of the channel and will add a conservative height for rescanning.
1 parent bc07d97 commit bed021f

File tree

2 files changed

+273
-1
lines changed

2 files changed

+273
-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+
}

0 commit comments

Comments
 (0)