diff --git a/contractcourt/chain_watcher_test.go b/contractcourt/chain_watcher_test.go new file mode 100644 index 00000000..7e65c5fa --- /dev/null +++ b/contractcourt/chain_watcher_test.go @@ -0,0 +1,205 @@ +package contractcourt + +import ( + "bytes" + "crypto/sha256" + "testing" + "time" + + "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/roasbeef/btcd/chaincfg/chainhash" + "github.com/roasbeef/btcd/wire" +) + +type mockNotifier struct { + spendChan chan *chainntnfs.SpendDetail +} + +func (m *mockNotifier) RegisterConfirmationsNtfn(txid *chainhash.Hash, numConfs, + heightHint uint32) (*chainntnfs.ConfirmationEvent, error) { + return nil, nil +} +func (m *mockNotifier) RegisterBlockEpochNtfn() (*chainntnfs.BlockEpochEvent, error) { + return &chainntnfs.BlockEpochEvent{ + Epochs: make(chan *chainntnfs.BlockEpoch), + Cancel: func() {}, + }, nil +} + +func (m *mockNotifier) Start() error { + return nil +} + +func (m *mockNotifier) Stop() error { + return nil +} +func (m *mockNotifier) RegisterSpendNtfn(outpoint *wire.OutPoint, + heightHint uint32, _ bool) (*chainntnfs.SpendEvent, error) { + return &chainntnfs.SpendEvent{ + Spend: m.spendChan, + Cancel: func() {}, + }, nil +} + +// TestChainWatcherRemoteUnilateralClose tests that the chain watcher is able +// to properly detect a normal unilateral close by the remote node using their +// lowest commitment. +func TestChainWatcherRemoteUnilateralClose(t *testing.T) { + t.Parallel() + + // First, we'll create two channels which already have established a + // commitment contract between themselves. + aliceChannel, bobChannel, cleanUp, err := lnwallet.CreateTestChannels() + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + // With the channels created, we'll now create a chain watcher instance + // which will be watching for any closes of Alice's channel. + aliceNotifier := &mockNotifier{ + spendChan: make(chan *chainntnfs.SpendDetail), + } + aliceChainWatcher, err := newChainWatcher( + aliceChannel.State(), aliceNotifier, nil, aliceChannel.Signer, + nil, nil, + ) + if err != nil { + t.Fatalf("unable to create chain watcher: %v", err) + } + err = aliceChainWatcher.Start() + if err != nil { + t.Fatalf("unable to start chain watcher: %v", err) + } + defer aliceChainWatcher.Stop() + + // We'll request a new channel event subscription from Alice's chain + // watcher. + chanEvents := aliceChainWatcher.SubscribeChannelEvents(false) + + // If we simulate an immediate broadcast of the current commitment by + // Bob, then the chain watcher should detect this case. + bobCommit := bobChannel.State().LocalCommitment.CommitTx + bobTxHash := bobCommit.TxHash() + bobSpend := &chainntnfs.SpendDetail{ + SpenderTxHash: &bobTxHash, + SpendingTx: bobCommit, + } + aliceNotifier.spendChan <- bobSpend + + // We should get a new spend event over the remote unilateral close + // event channel. + var uniClose *lnwallet.UnilateralCloseSummary + select { + case uniClose = <-chanEvents.RemoteUnilateralClosure: + case <-time.After(time.Second * 15): + t.Fatalf("didn't receive unilateral close event") + } + + // The unilateral close should have properly located Alice's output in + // the commitment transaction. + if uniClose.CommitResolution == nil { + t.Fatalf("unable to find alice's commit resolution") + } +} + +// TestChainWatcherRemoteUnilateralClosePendingCommit tests that the chain +// watcher is able to properly detect a unilateral close wherein the remote +// node broadcasts their newly received commitment, without first revoking the +// old one. +func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) { + t.Parallel() + + // First, we'll create two channels which already have established a + // commitment contract between themselves. + aliceChannel, bobChannel, cleanUp, err := lnwallet.CreateTestChannels() + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + // With the channels created, we'll now create a chain watcher instance + // which will be watching for any closes of Alice's channel. + aliceNotifier := &mockNotifier{ + spendChan: make(chan *chainntnfs.SpendDetail), + } + aliceChainWatcher, err := newChainWatcher( + aliceChannel.State(), aliceNotifier, nil, aliceChannel.Signer, + nil, nil, + ) + if err != nil { + t.Fatalf("unable to create chain watcher: %v", err) + } + if err := aliceChainWatcher.Start(); err != nil { + t.Fatalf("unable to start chain watcher: %v", err) + } + defer aliceChainWatcher.Stop() + + // We'll request a new channel event subscription from Alice's chain + // watcher. + chanEvents := aliceChainWatcher.SubscribeChannelEvents(false) + + // Next, we'll create a fake HTLC just so we can advance Alice's + // channel state to a new pending commitment on her remote commit chain + // for Bob. + htlcAmount := lnwire.NewMSatFromSatoshis(20000) + preimage := bytes.Repeat([]byte{byte(1)}, 32) + paymentHash := sha256.Sum256(preimage) + var returnPreimage [32]byte + copy(returnPreimage[:], preimage) + htlc := &lnwire.UpdateAddHTLC{ + ID: uint64(0), + PaymentHash: paymentHash, + Amount: htlcAmount, + Expiry: uint32(5), + } + + if _, err := aliceChannel.AddHTLC(htlc, nil); err != nil { + t.Fatalf("alice unable to add htlc: %v", err) + } + if _, err := bobChannel.ReceiveHTLC(htlc); err != nil { + t.Fatalf("bob unable to recv add htlc: %v", err) + } + + // With the HTLC added, we'll now manually initiate a state transition + // from Alice to Bob. + _, _, err = aliceChannel.SignNextCommitment() + if err != nil { + t.Fatal(err) + } + + // At this point, we'll now Bob broadcasting this new pending unrevoked + // commitment. + bobPendingCommit, err := aliceChannel.State().RemoteCommitChainTip() + if err != nil { + t.Fatal(err) + } + + // We'll craft a fake spend notification with Bob's actual commitment. + // The chain watcher should be able to detect that this is a pending + // commit broadcast based on the state hints in the commitment. + bobCommit := bobPendingCommit.Commitment.CommitTx + bobTxHash := bobCommit.TxHash() + bobSpend := &chainntnfs.SpendDetail{ + SpenderTxHash: &bobTxHash, + SpendingTx: bobCommit, + } + aliceNotifier.spendChan <- bobSpend + + // We should get a new spend event over the remote unilateral close + // event channel. + var uniClose *lnwallet.UnilateralCloseSummary + select { + case uniClose = <-chanEvents.RemoteUnilateralClosure: + case <-time.After(time.Second * 15): + t.Fatalf("didn't receive unilateral close event") + } + + // The unilateral close should have properly located Alice's output in + // the commitment transaction. + if uniClose.CommitResolution == nil { + t.Fatalf("unable to find alice's commit resolution") + } +}