|  | 
| 1 | 1 | package sweepbatcher | 
| 2 | 2 | 
 | 
| 3 | 3 | import ( | 
|  | 4 | +	"bytes" | 
| 4 | 5 | 	"context" | 
| 5 | 6 | 	"fmt" | 
| 6 | 7 | 	"os" | 
| @@ -1268,6 +1269,156 @@ func testPresigned_presigned_group_with_change(t *testing.T, | 
| 1268 | 1269 | 	require.NoError(t, lnd.NotifyHeight(601)) | 
| 1269 | 1270 | } | 
| 1270 | 1271 | 
 | 
|  | 1272 | +// testPresigned_fee_portion_with_change ensures that the fee portion reported | 
|  | 1273 | +// to clients accounts for change outputs in the presigned transaction. It also | 
|  | 1274 | +// is a regression test for feerate overestimation when tx is published. | 
|  | 1275 | +func testPresigned_fee_portion_with_change(t *testing.T, | 
|  | 1276 | +	batcherStore testBatcherStore) { | 
|  | 1277 | + | 
|  | 1278 | +	defer test.Guard(t)() | 
|  | 1279 | + | 
|  | 1280 | +	lnd := test.NewMockLnd() | 
|  | 1281 | + | 
|  | 1282 | +	ctx, cancel := context.WithCancel(context.Background()) | 
|  | 1283 | +	defer cancel() | 
|  | 1284 | + | 
|  | 1285 | +	customFeeRate := func(_ context.Context, _ lntypes.Hash, | 
|  | 1286 | +		_ wire.OutPoint) (chainfee.SatPerKWeight, error) { | 
|  | 1287 | + | 
|  | 1288 | +		return chainfee.SatPerKWeight(10_000), nil | 
|  | 1289 | +	} | 
|  | 1290 | + | 
|  | 1291 | +	presignedHelper := newMockPresignedHelper() | 
|  | 1292 | + | 
|  | 1293 | +	batcher := NewBatcher( | 
|  | 1294 | +		lnd.WalletKit, lnd.ChainNotifier, lnd.Signer, | 
|  | 1295 | +		testMuSig2SignSweep, testVerifySchnorrSig, lnd.ChainParams, | 
|  | 1296 | +		batcherStore, presignedHelper, | 
|  | 1297 | +		WithCustomFeeRate(customFeeRate), | 
|  | 1298 | +		WithPresignedHelper(presignedHelper), | 
|  | 1299 | +	) | 
|  | 1300 | + | 
|  | 1301 | +	go func() { | 
|  | 1302 | +		err := batcher.Run(ctx) | 
|  | 1303 | +		checkBatcherError(t, err) | 
|  | 1304 | +	}() | 
|  | 1305 | + | 
|  | 1306 | +	swapHash := lntypes.Hash{2, 2, 2} | 
|  | 1307 | +	op := wire.OutPoint{ | 
|  | 1308 | +		Hash:  chainhash.Hash{2, 2}, | 
|  | 1309 | +		Index: 2, | 
|  | 1310 | +	} | 
|  | 1311 | +	group := []Input{ | 
|  | 1312 | +		{ | 
|  | 1313 | +			Outpoint: op, | 
|  | 1314 | +			Value:    1_000_000, | 
|  | 1315 | +		}, | 
|  | 1316 | +	} | 
|  | 1317 | +	change := &wire.TxOut{ | 
|  | 1318 | +		Value:    250_000, | 
|  | 1319 | +		PkScript: []byte{0xca, 0xfe}, | 
|  | 1320 | +	} | 
|  | 1321 | + | 
|  | 1322 | +	presignedHelper.setChangeForPrimaryDeposit(op, change) | 
|  | 1323 | +	presignedHelper.SetOutpointOnline(op, true) | 
|  | 1324 | + | 
|  | 1325 | +	require.NoError(t, batcher.PresignSweepsGroup( | 
|  | 1326 | +		ctx, group, sweepTimeout, destAddr, change, | 
|  | 1327 | +	)) | 
|  | 1328 | + | 
|  | 1329 | +	spendChan := make(chan *SpendDetail, 1) | 
|  | 1330 | +	confChan := make(chan *ConfDetail, 1) | 
|  | 1331 | +	notifier := &SpendNotifier{ | 
|  | 1332 | +		SpendChan:    spendChan, | 
|  | 1333 | +		SpendErrChan: make(chan error, 1), | 
|  | 1334 | +		ConfChan:     confChan, | 
|  | 1335 | +		ConfErrChan:  make(chan error, 1), | 
|  | 1336 | +		QuitChan:     make(chan bool, 1), | 
|  | 1337 | +	} | 
|  | 1338 | + | 
|  | 1339 | +	require.NoError(t, batcher.AddSweep(ctx, &SweepRequest{ | 
|  | 1340 | +		SwapHash: swapHash, | 
|  | 1341 | +		Inputs:   group, | 
|  | 1342 | +		Notifier: notifier, | 
|  | 1343 | +	})) | 
|  | 1344 | + | 
|  | 1345 | +	spendReg := <-lnd.RegisterSpendChannel | 
|  | 1346 | +	require.NotNil(t, spendReg) | 
|  | 1347 | +	require.NotNil(t, spendReg.Outpoint) | 
|  | 1348 | +	require.Equal(t, op, *spendReg.Outpoint) | 
|  | 1349 | + | 
|  | 1350 | +	tx := <-lnd.TxPublishChannel | 
|  | 1351 | +	require.Len(t, tx.TxIn, 1) | 
|  | 1352 | +	require.Len(t, tx.TxOut, 2) | 
|  | 1353 | + | 
|  | 1354 | +	// Mine a blocks to trigger republishing. | 
|  | 1355 | +	require.NoError(t, lnd.NotifyHeight(601)) | 
|  | 1356 | + | 
|  | 1357 | +	// Make sure it is the same tx. | 
|  | 1358 | +	tx2 := <-lnd.TxPublishChannel | 
|  | 1359 | +	require.Len(t, tx2.TxOut, len(tx.TxOut)) | 
|  | 1360 | +	require.Equal(t, tx.TxOut[0].Value, tx2.TxOut[0].Value) | 
|  | 1361 | + | 
|  | 1362 | +	var ( | 
|  | 1363 | +		outputSum   int64 | 
|  | 1364 | +		foundChange bool | 
|  | 1365 | +	) | 
|  | 1366 | +	for _, txOut := range tx.TxOut { | 
|  | 1367 | +		outputSum += txOut.Value | 
|  | 1368 | +		if txOut.Value != change.Value { | 
|  | 1369 | +			continue | 
|  | 1370 | +		} | 
|  | 1371 | + | 
|  | 1372 | +		if !bytes.Equal(txOut.PkScript, change.PkScript) { | 
|  | 1373 | +			continue | 
|  | 1374 | +		} | 
|  | 1375 | + | 
|  | 1376 | +		foundChange = true | 
|  | 1377 | +	} | 
|  | 1378 | + | 
|  | 1379 | +	require.True(t, foundChange) | 
|  | 1380 | + | 
|  | 1381 | +	totalInput := int64(group[0].Value) | 
|  | 1382 | +	require.LessOrEqual(t, outputSum, totalInput) | 
|  | 1383 | + | 
|  | 1384 | +	expectedFee := btcutil.Amount(totalInput - outputSum) | 
|  | 1385 | +	require.Greater(t, expectedFee, btcutil.Amount(0)) | 
|  | 1386 | + | 
|  | 1387 | +	txHash := tx.TxHash() | 
|  | 1388 | +	spendDetail := &chainntnfs.SpendDetail{ | 
|  | 1389 | +		SpentOutPoint:     &op, | 
|  | 1390 | +		SpendingTx:        tx, | 
|  | 1391 | +		SpenderTxHash:     &txHash, | 
|  | 1392 | +		SpenderInputIndex: 0, | 
|  | 1393 | +		SpendingHeight:    spendReg.HeightHint + 1, | 
|  | 1394 | +	} | 
|  | 1395 | +	lnd.SpendChannel <- spendDetail | 
|  | 1396 | + | 
|  | 1397 | +	spend := <-spendChan | 
|  | 1398 | +	require.Equal(t, expectedFee, spend.OnChainFeePortion) | 
|  | 1399 | + | 
|  | 1400 | +	confReg := <-lnd.RegisterConfChannel | 
|  | 1401 | +	require.True(t, bytes.Equal(tx.TxOut[0].PkScript, confReg.PkScript) || | 
|  | 1402 | +		bytes.Equal(tx.TxOut[1].PkScript, confReg.PkScript)) | 
|  | 1403 | + | 
|  | 1404 | +	require.NoError( | 
|  | 1405 | +		t, lnd.NotifyHeight(spendReg.HeightHint+batchConfHeight+1), | 
|  | 1406 | +	) | 
|  | 1407 | +	lnd.ConfChannel <- &chainntnfs.TxConfirmation{Tx: tx} | 
|  | 1408 | + | 
|  | 1409 | +	require.Eventually(t, func() bool { | 
|  | 1410 | +		select { | 
|  | 1411 | +		case <-presignedHelper.cleanupCalled: | 
|  | 1412 | +			return true | 
|  | 1413 | +		default: | 
|  | 1414 | +			return false | 
|  | 1415 | +		} | 
|  | 1416 | +	}, test.Timeout, eventuallyCheckFrequency) | 
|  | 1417 | + | 
|  | 1418 | +	conf := <-confChan | 
|  | 1419 | +	require.Equal(t, expectedFee, conf.OnChainFeePortion) | 
|  | 1420 | +} | 
|  | 1421 | + | 
| 1271 | 1422 | // testPresigned_presigned_group_with_identical_change_pkscript tests passing multiple sweeps to | 
| 1272 | 1423 | // the method PresignSweepsGroup. It tests that a change output of a primary | 
| 1273 | 1424 | // deposit sweep is properly added to the presigned transaction. | 
| @@ -2356,6 +2507,10 @@ func TestPresigned(t *testing.T) { | 
| 2356 | 2507 | 		testPresigned_presigned_group_with_change(t, NewStoreMock()) | 
| 2357 | 2508 | 	}) | 
| 2358 | 2509 | 
 | 
|  | 2510 | +	t.Run("fee_portion_change", func(t *testing.T) { | 
|  | 2511 | +		testPresigned_fee_portion_with_change(t, NewStoreMock()) | 
|  | 2512 | +	}) | 
|  | 2513 | + | 
| 2359 | 2514 | 	t.Run("identical change pkscript", func(t *testing.T) { | 
| 2360 | 2515 | 		testPresigned_presigned_group_with_identical_change_pkscript(t, NewStoreMock()) | 
| 2361 | 2516 | 	}) | 
|  | 
0 commit comments