|
1 | 1 | package itest
|
2 | 2 |
|
3 | 3 | import (
|
| 4 | + "context" |
4 | 5 | "encoding/hex"
|
5 | 6 | "fmt"
|
6 | 7 | "os"
|
7 | 8 |
|
8 | 9 | "github.com/btcsuite/btcd/btcutil"
|
| 10 | + "github.com/btcsuite/btcd/chaincfg/chainhash" |
9 | 11 | "github.com/btcsuite/btcd/txscript"
|
10 | 12 | "github.com/btcsuite/btcd/wire"
|
11 | 13 | "github.com/btcsuite/btcwallet/wallet"
|
12 | 14 | "github.com/lightningnetwork/lnd/funding"
|
13 | 15 | "github.com/lightningnetwork/lnd/input"
|
14 | 16 | "github.com/lightningnetwork/lnd/lncfg"
|
15 | 17 | "github.com/lightningnetwork/lnd/lnrpc"
|
| 18 | + "github.com/lightningnetwork/lnd/lnrpc/chainrpc" |
16 | 19 | "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
17 | 20 | "github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
18 | 21 | "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
19 | 22 | "github.com/lightningnetwork/lnd/lntest"
|
20 | 23 | "github.com/lightningnetwork/lnd/lntest/node"
|
| 24 | + "github.com/lightningnetwork/lnd/lntest/rpc" |
21 | 25 | "github.com/lightningnetwork/lnd/lntest/wait"
|
22 | 26 | "github.com/lightningnetwork/lnd/lnwallet"
|
23 | 27 | "github.com/lightningnetwork/lnd/lnwire"
|
@@ -1391,3 +1395,203 @@ func testGRPCNotFound(ht *lntest.HarnessTest) {
|
1391 | 1395 | RHash: rHash,
|
1392 | 1396 | }, notFoundErr)
|
1393 | 1397 | }
|
| 1398 | + |
| 1399 | +// receiveSpendNotification tries to receive a spend notification from a spend |
| 1400 | +// client until the context expires. |
| 1401 | +func receiveSpendNotification(ctx context.Context, |
| 1402 | + spendClient rpc.SpendClient) (*chainrpc.SpendEvent, error) { |
| 1403 | + |
| 1404 | + var ( |
| 1405 | + msg *chainrpc.SpendEvent |
| 1406 | + recvErr error |
| 1407 | + ) |
| 1408 | + |
| 1409 | + received := make(chan struct{}) |
| 1410 | + go func() { |
| 1411 | + msg, recvErr = spendClient.Recv() |
| 1412 | + close(received) |
| 1413 | + }() |
| 1414 | + select { |
| 1415 | + case <-ctx.Done(): |
| 1416 | + return nil, fmt.Errorf("spending notification expired") |
| 1417 | + |
| 1418 | + case <-received: |
| 1419 | + return msg, recvErr |
| 1420 | + } |
| 1421 | +} |
| 1422 | + |
| 1423 | +// testReorgNotifications tests that RegisterSpendNtfn behaves as expected |
| 1424 | +// during a reorg. A reorg notification is produced after a reorg affects the |
| 1425 | +// block which has produced a spending notification for this registration. |
| 1426 | +func testReorgNotifications(ht *lntest.HarnessTest) { |
| 1427 | + ctxb := context.Background() |
| 1428 | + const timeout = wait.DefaultTimeout |
| 1429 | + |
| 1430 | + alice := ht.NewNodeWithCoins("Alice", nil) |
| 1431 | + bob := ht.NewNode("Bob", nil) |
| 1432 | + |
| 1433 | + const tx1Amount = 1_000_000 |
| 1434 | + |
| 1435 | + // Alice will send coins to herself, Bob will watch spending and |
| 1436 | + // confirmation of the transaction. We make sure that a node can watch |
| 1437 | + // transactions which are not a part of its wallet. |
| 1438 | + respAddr := alice.RPC.NewAddress(&lnrpc.NewAddressRequest{ |
| 1439 | + Type: lnrpc.AddressType_TAPROOT_PUBKEY, |
| 1440 | + }) |
| 1441 | + txid1Str := alice.RPC.SendCoins(&lnrpc.SendCoinsRequest{ |
| 1442 | + Addr: respAddr.Address, |
| 1443 | + Amount: tx1Amount, |
| 1444 | + SatPerVbyte: 2, |
| 1445 | + }).Txid |
| 1446 | + txid1, err := chainhash.NewHashFromStr(txid1Str) |
| 1447 | + require.NoError(ht, err) |
| 1448 | + tx1 := ht.AssertTxInMempool(*txid1) |
| 1449 | + |
| 1450 | + // Find the output of tx1. |
| 1451 | + tx1OutIndex := -1 |
| 1452 | + for i, txOut := range tx1.TxOut { |
| 1453 | + if txOut.Value == tx1Amount { |
| 1454 | + tx1OutIndex = i |
| 1455 | + } |
| 1456 | + } |
| 1457 | + require.NotEqual(ht, -1, tx1OutIndex) |
| 1458 | + tx1op := wire.OutPoint{ |
| 1459 | + Hash: *txid1, |
| 1460 | + Index: uint32(tx1OutIndex), |
| 1461 | + } |
| 1462 | + tx1opLnrpc := &lnrpc.OutPoint{ |
| 1463 | + TxidStr: txid1Str, |
| 1464 | + OutputIndex: uint32(tx1OutIndex), |
| 1465 | + } |
| 1466 | + tx1opChainrpc := &chainrpc.Outpoint{ |
| 1467 | + Hash: txid1[:], |
| 1468 | + Index: uint32(tx1OutIndex), |
| 1469 | + } |
| 1470 | + pkscript := tx1.TxOut[tx1OutIndex].PkScript |
| 1471 | + |
| 1472 | + // Now fee bump the output of the first transaction. |
| 1473 | + alice.RPC.BumpFee(&walletrpc.BumpFeeRequest{ |
| 1474 | + Outpoint: tx1opLnrpc, |
| 1475 | + Immediate: true, |
| 1476 | + SatPerVbyte: 20, |
| 1477 | + }) |
| 1478 | + |
| 1479 | + // Now find the fee bump tx. |
| 1480 | + listSweepsReq := &walletrpc.ListSweepsRequest{ |
| 1481 | + Verbose: true, |
| 1482 | + |
| 1483 | + // startHeight -1 means include only unconfirmed. |
| 1484 | + StartHeight: -1, |
| 1485 | + } |
| 1486 | + |
| 1487 | + var tx2aLnrpc *lnrpc.Transaction |
| 1488 | + require.NoError(ht, wait.NoError(func() error { |
| 1489 | + sweepsResp := alice.RPC.ListSweeps(listSweepsReq) |
| 1490 | + sweepsDetails := sweepsResp.GetTransactionDetails() |
| 1491 | + if sweepsDetails == nil { |
| 1492 | + return fmt.Errorf("no sweep details") |
| 1493 | + } |
| 1494 | + if len(sweepsDetails.Transactions) != 1 { |
| 1495 | + return fmt.Errorf("got %d sweeps, want %d", |
| 1496 | + len(sweepsDetails.Transactions), 1) |
| 1497 | + } |
| 1498 | + tx2aLnrpc = sweepsDetails.Transactions[0] |
| 1499 | + |
| 1500 | + return nil |
| 1501 | + }, defaultTimeout)) |
| 1502 | + |
| 1503 | + require.Len(ht, tx2aLnrpc.PreviousOutpoints, 1) |
| 1504 | + require.Equal( |
| 1505 | + ht, tx1op.String(), tx2aLnrpc.PreviousOutpoints[0].Outpoint, |
| 1506 | + ) |
| 1507 | + txid2a, err := chainhash.NewHashFromStr(tx2aLnrpc.TxHash) |
| 1508 | + require.NoError(ht, err) |
| 1509 | + tx2a := ht.AssertTxInMempool(*txid2a) |
| 1510 | + |
| 1511 | + // Fee bump the output of the first transaction again with a higher fee |
| 1512 | + // rate to get RBF transaction tx2b. |
| 1513 | + alice.RPC.BumpFee(&walletrpc.BumpFeeRequest{ |
| 1514 | + Outpoint: tx1opLnrpc, |
| 1515 | + Immediate: true, |
| 1516 | + SatPerVbyte: 200, |
| 1517 | + }) |
| 1518 | + |
| 1519 | + var tx2bLnrpc *lnrpc.Transaction |
| 1520 | + require.NoError(ht, wait.NoError(func() error { |
| 1521 | + sweepsResp := alice.RPC.ListSweeps(listSweepsReq) |
| 1522 | + sweepsDetails := sweepsResp.GetTransactionDetails() |
| 1523 | + if sweepsDetails == nil { |
| 1524 | + return fmt.Errorf("no sweep details") |
| 1525 | + } |
| 1526 | + for _, tx := range sweepsDetails.Transactions { |
| 1527 | + if tx.TxHash != tx2aLnrpc.TxHash { |
| 1528 | + tx2bLnrpc = tx |
| 1529 | + break |
| 1530 | + } |
| 1531 | + } |
| 1532 | + if tx2bLnrpc == nil { |
| 1533 | + return fmt.Errorf("tx2aLnrpc hasn't been replaced yet") |
| 1534 | + } |
| 1535 | + |
| 1536 | + return nil |
| 1537 | + }, defaultTimeout)) |
| 1538 | + |
| 1539 | + require.Len(ht, tx2bLnrpc.PreviousOutpoints, 1) |
| 1540 | + require.Equal( |
| 1541 | + ht, tx1op.String(), tx2bLnrpc.PreviousOutpoints[0].Outpoint, |
| 1542 | + ) |
| 1543 | + txid2b, err := chainhash.NewHashFromStr(tx2bLnrpc.TxHash) |
| 1544 | + require.NoError(ht, err) |
| 1545 | + tx2b := ht.AssertTxInMempool(*txid2b) |
| 1546 | + |
| 1547 | + // Mine tx1 only. |
| 1548 | + ht.Miner().MineBlockWithTxes([]*btcutil.Tx{btcutil.NewTx(tx1)}) |
| 1549 | + |
| 1550 | + // Bob starts watching spending of tx1op. |
| 1551 | + spendClient := bob.RPC.RegisterSpendNtfn(&chainrpc.SpendRequest{ |
| 1552 | + Outpoint: tx1opChainrpc, |
| 1553 | + Script: pkscript, |
| 1554 | + HeightHint: ht.CurrentHeight(), |
| 1555 | + }) |
| 1556 | + |
| 1557 | + // Mine tx2b. |
| 1558 | + block1 := ht.Miner().MineBlockWithTxes( |
| 1559 | + []*btcutil.Tx{btcutil.NewTx(tx2b)}, |
| 1560 | + ) |
| 1561 | + |
| 1562 | + // Make sure RegisterSpendNtfn noticed the spending. |
| 1563 | + ctx, cancel := context.WithTimeout(ctxb, timeout) |
| 1564 | + defer cancel() |
| 1565 | + spendMsg, err := receiveSpendNotification(ctx, spendClient) |
| 1566 | + require.NoError(ht, err) |
| 1567 | + spendDetails := spendMsg.GetSpend() |
| 1568 | + require.NotNil(ht, spendDetails) |
| 1569 | + require.Equal(ht, txid2b[:], spendDetails.SpendingTxHash) |
| 1570 | + |
| 1571 | + // Reorg block1. |
| 1572 | + blockHash1 := block1.Header.BlockHash() |
| 1573 | + require.NoError(ht, ht.Miner().Client.InvalidateBlock(&blockHash1)) |
| 1574 | + |
| 1575 | + // Mine empty blocks to evict block1 in bitcoin backend (e.g. bitcoind). |
| 1576 | + ht.Miner().MineEmptyBlocks(2) |
| 1577 | + |
| 1578 | + // Make sure RegisterSpendNtfn noticed the reorg. Transaction tx2b was |
| 1579 | + // just unconfirmed. |
| 1580 | + ctx, cancel = context.WithTimeout(ctxb, timeout) |
| 1581 | + defer cancel() |
| 1582 | + spendMsg, err = receiveSpendNotification(ctx, spendClient) |
| 1583 | + require.NoError(ht, err) |
| 1584 | + require.NotNil(ht, spendMsg.GetReorg()) |
| 1585 | + |
| 1586 | + // Mine tx2a to confirm a different version of spending. |
| 1587 | + ht.Miner().MineBlockWithTxes([]*btcutil.Tx{btcutil.NewTx(tx2a)}) |
| 1588 | + |
| 1589 | + // Make sure RegisterSpendNtfn noticed the spending. |
| 1590 | + ctx, cancel = context.WithTimeout(ctxb, timeout) |
| 1591 | + defer cancel() |
| 1592 | + spendMsg, err = receiveSpendNotification(ctx, spendClient) |
| 1593 | + require.NoError(ht, err) |
| 1594 | + spendDetails = spendMsg.GetSpend() |
| 1595 | + require.NotNil(ht, spendDetails) |
| 1596 | + require.Equal(ht, txid2a[:], spendDetails.SpendingTxHash) |
| 1597 | +} |
0 commit comments