You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
627 lines
19 KiB
627 lines
19 KiB
package contractcourt |
|
|
|
import ( |
|
"bytes" |
|
"crypto/sha256" |
|
"fmt" |
|
"testing" |
|
"time" |
|
|
|
"github.com/btcsuite/btcd/wire" |
|
"github.com/lightningnetwork/lnd/chainntnfs" |
|
"github.com/lightningnetwork/lnd/channeldb" |
|
"github.com/lightningnetwork/lnd/input" |
|
"github.com/lightningnetwork/lnd/lntest/mock" |
|
"github.com/lightningnetwork/lnd/lnwallet" |
|
"github.com/lightningnetwork/lnd/lnwire" |
|
) |
|
|
|
// 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( |
|
channeldb.SingleFunderTweaklessBit, |
|
) |
|
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 := &mock.ChainNotifier{ |
|
SpendChan: make(chan *chainntnfs.SpendDetail), |
|
EpochChan: make(chan *chainntnfs.BlockEpoch), |
|
ConfChan: make(chan *chainntnfs.TxConfirmation), |
|
} |
|
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{ |
|
chanState: aliceChannel.State(), |
|
notifier: aliceNotifier, |
|
signer: aliceChannel.Signer, |
|
extractStateNumHint: lnwallet.GetStateNumHint, |
|
}) |
|
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() |
|
|
|
// 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 *RemoteUnilateralCloseInfo |
|
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") |
|
} |
|
} |
|
|
|
func addFakeHTLC(t *testing.T, htlcAmount lnwire.MilliSatoshi, id uint64, |
|
aliceChannel, bobChannel *lnwallet.LightningChannel) { |
|
|
|
preimage := bytes.Repeat([]byte{byte(id)}, 32) |
|
paymentHash := sha256.Sum256(preimage) |
|
var returnPreimage [32]byte |
|
copy(returnPreimage[:], preimage) |
|
htlc := &lnwire.UpdateAddHTLC{ |
|
ID: uint64(id), |
|
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) |
|
} |
|
} |
|
|
|
// 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( |
|
channeldb.SingleFunderTweaklessBit, |
|
) |
|
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 := &mock.ChainNotifier{ |
|
SpendChan: make(chan *chainntnfs.SpendDetail), |
|
EpochChan: make(chan *chainntnfs.BlockEpoch), |
|
ConfChan: make(chan *chainntnfs.TxConfirmation), |
|
} |
|
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{ |
|
chanState: aliceChannel.State(), |
|
notifier: aliceNotifier, |
|
signer: aliceChannel.Signer, |
|
extractStateNumHint: lnwallet.GetStateNumHint, |
|
}) |
|
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() |
|
|
|
// 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) |
|
addFakeHTLC(t, htlcAmount, 0, aliceChannel, bobChannel) |
|
|
|
// 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 *RemoteUnilateralCloseInfo |
|
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") |
|
} |
|
} |
|
|
|
// dlpTestCase is a special struct that we'll use to generate randomized test |
|
// cases for the main TestChainWatcherDataLossProtect test. This struct has a |
|
// special Generate method that will generate a random state number, and a |
|
// broadcast state number which is greater than that state number. |
|
type dlpTestCase struct { |
|
BroadcastStateNum uint8 |
|
NumUpdates uint8 |
|
} |
|
|
|
// executeStateTransitions execute the given number of state transitions. |
|
// Copies of Alice's channel state before each transition (including initial |
|
// state) are returned. |
|
func executeStateTransitions(t *testing.T, htlcAmount lnwire.MilliSatoshi, |
|
aliceChannel, bobChannel *lnwallet.LightningChannel, |
|
numUpdates uint8) ([]*channeldb.OpenChannel, func(), error) { |
|
|
|
// We'll make a copy of the channel state before each transition. |
|
var ( |
|
chanStates []*channeldb.OpenChannel |
|
cleanupFuncs []func() |
|
) |
|
|
|
cleanAll := func() { |
|
for _, f := range cleanupFuncs { |
|
f() |
|
} |
|
} |
|
|
|
state, f, err := copyChannelState(aliceChannel.State()) |
|
if err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
chanStates = append(chanStates, state) |
|
cleanupFuncs = append(cleanupFuncs, f) |
|
|
|
for i := 0; i < int(numUpdates); i++ { |
|
addFakeHTLC( |
|
t, htlcAmount, uint64(i), aliceChannel, bobChannel, |
|
) |
|
|
|
err := lnwallet.ForceStateTransition(aliceChannel, bobChannel) |
|
if err != nil { |
|
cleanAll() |
|
return nil, nil, err |
|
} |
|
|
|
state, f, err := copyChannelState(aliceChannel.State()) |
|
if err != nil { |
|
cleanAll() |
|
return nil, nil, err |
|
} |
|
|
|
chanStates = append(chanStates, state) |
|
cleanupFuncs = append(cleanupFuncs, f) |
|
} |
|
|
|
return chanStates, cleanAll, nil |
|
} |
|
|
|
// TestChainWatcherDataLossProtect tests that if we've lost data (and are |
|
// behind the remote node), then we'll properly detect this case and dispatch a |
|
// remote force close using the obtained data loss commitment point. |
|
func TestChainWatcherDataLossProtect(t *testing.T) { |
|
t.Parallel() |
|
|
|
// dlpScenario is our primary quick check testing function for this |
|
// test as whole. It ensures that if the remote party broadcasts a |
|
// commitment that is beyond our best known commitment for them, and |
|
// they don't have a pending commitment (one we sent but which hasn't |
|
// been revoked), then we'll properly detect this case, and execute the |
|
// DLP protocol on our end. |
|
// |
|
// broadcastStateNum is the number that we'll trick Alice into thinking |
|
// was broadcast, while numUpdates is the actual number of updates |
|
// we'll execute. Both of these will be random 8-bit values generated |
|
// by testing/quick. |
|
dlpScenario := func(t *testing.T, testCase dlpTestCase) bool { |
|
// First, we'll create two channels which already have |
|
// established a commitment contract between themselves. |
|
aliceChannel, bobChannel, cleanUp, err := lnwallet.CreateTestChannels( |
|
channeldb.SingleFunderBit, |
|
) |
|
if err != nil { |
|
t.Fatalf("unable to create test channels: %v", err) |
|
} |
|
defer cleanUp() |
|
|
|
// Based on the number of random updates for this state, make a |
|
// new HTLC to add to the commitment, and then lock in a state |
|
// transition. |
|
const htlcAmt = 1000 |
|
states, cleanStates, err := executeStateTransitions( |
|
t, htlcAmt, aliceChannel, bobChannel, |
|
testCase.BroadcastStateNum, |
|
) |
|
if err != nil { |
|
t.Errorf("unable to trigger state "+ |
|
"transition: %v", err) |
|
return false |
|
} |
|
defer cleanStates() |
|
|
|
// We'll use the state this test case wants Alice to start at. |
|
aliceChanState := states[testCase.NumUpdates] |
|
|
|
// With the channels created, we'll now create a chain watcher |
|
// instance which will be watching for any closes of Alice's |
|
// channel. |
|
aliceNotifier := &mock.ChainNotifier{ |
|
SpendChan: make(chan *chainntnfs.SpendDetail), |
|
EpochChan: make(chan *chainntnfs.BlockEpoch), |
|
ConfChan: make(chan *chainntnfs.TxConfirmation), |
|
} |
|
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{ |
|
chanState: aliceChanState, |
|
notifier: aliceNotifier, |
|
signer: aliceChannel.Signer, |
|
extractStateNumHint: func(*wire.MsgTx, |
|
[lnwallet.StateHintSize]byte) uint64 { |
|
|
|
// We'll return the "fake" broadcast commitment |
|
// number so we can simulate broadcast of an |
|
// arbitrary state. |
|
return uint64(testCase.BroadcastStateNum) |
|
}, |
|
}) |
|
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 so we can be notified of our fake close below. |
|
chanEvents := aliceChainWatcher.SubscribeChannelEvents() |
|
|
|
// Otherwise, we'll feed in this new state number as a response |
|
// to the query, and insert the expected DLP commit point. |
|
dlpPoint := aliceChannel.State().RemoteCurrentRevocation |
|
err = aliceChanState.MarkDataLoss(dlpPoint) |
|
if err != nil { |
|
t.Errorf("unable to insert dlp point: %v", err) |
|
return false |
|
} |
|
|
|
// Now we'll trigger the channel close event to trigger the |
|
// scenario. |
|
bobCommit := bobChannel.State().LocalCommitment.CommitTx |
|
bobTxHash := bobCommit.TxHash() |
|
bobSpend := &chainntnfs.SpendDetail{ |
|
SpenderTxHash: &bobTxHash, |
|
SpendingTx: bobCommit, |
|
} |
|
aliceNotifier.SpendChan <- bobSpend |
|
|
|
// We should get a new uni close resolution that indicates we |
|
// processed the DLP scenario. |
|
var uniClose *RemoteUnilateralCloseInfo |
|
select { |
|
case uniClose = <-chanEvents.RemoteUnilateralClosure: |
|
// If we processed this as a DLP case, then the remote |
|
// party's commitment should be blank, as we don't have |
|
// this up to date state. |
|
blankCommit := channeldb.ChannelCommitment{} |
|
if uniClose.RemoteCommit.FeePerKw != blankCommit.FeePerKw { |
|
t.Errorf("DLP path not executed") |
|
return false |
|
} |
|
|
|
// The resolution should have also read the DLP point |
|
// we stored above, and used that to derive their sweep |
|
// key for this output. |
|
sweepTweak := input.SingleTweakBytes( |
|
dlpPoint, |
|
aliceChannel.State().LocalChanCfg.PaymentBasePoint.PubKey, |
|
) |
|
commitResolution := uniClose.CommitResolution |
|
resolutionTweak := commitResolution.SelfOutputSignDesc.SingleTweak |
|
if !bytes.Equal(sweepTweak, resolutionTweak) { |
|
t.Errorf("sweep key mismatch: expected %x got %x", |
|
sweepTweak, resolutionTweak) |
|
return false |
|
} |
|
|
|
return true |
|
|
|
case <-time.After(time.Second * 5): |
|
t.Errorf("didn't receive unilateral close event") |
|
return false |
|
} |
|
} |
|
|
|
testCases := []dlpTestCase{ |
|
// For our first scenario, we'll ensure that if we're on state 1, |
|
// and the remote party broadcasts state 2 and we don't have a |
|
// pending commit for them, then we'll properly detect this as a |
|
// DLP scenario. |
|
{ |
|
BroadcastStateNum: 2, |
|
NumUpdates: 1, |
|
}, |
|
|
|
// We've completed a single update, but the remote party broadcasts |
|
// a state that's 5 states byeond our best known state. We've lost |
|
// data, but only partially, so we should enter a DLP secnario. |
|
{ |
|
BroadcastStateNum: 6, |
|
NumUpdates: 1, |
|
}, |
|
|
|
// Similar to the case above, but we've done more than one |
|
// update. |
|
{ |
|
BroadcastStateNum: 6, |
|
NumUpdates: 3, |
|
}, |
|
|
|
// We've done zero updates, but our channel peer broadcasts a |
|
// state beyond our knowledge. |
|
{ |
|
BroadcastStateNum: 10, |
|
NumUpdates: 0, |
|
}, |
|
} |
|
for _, testCase := range testCases { |
|
testName := fmt.Sprintf("num_updates=%v,broadcast_state_num=%v", |
|
testCase.NumUpdates, testCase.BroadcastStateNum) |
|
|
|
testCase := testCase |
|
t.Run(testName, func(t *testing.T) { |
|
t.Parallel() |
|
|
|
if !dlpScenario(t, testCase) { |
|
t.Fatalf("test %v failed", testName) |
|
} |
|
}) |
|
} |
|
} |
|
|
|
// TestChainWatcherLocalForceCloseDetect tests we're able to always detect our |
|
// commitment output based on only the outputs present on the transaction. |
|
func TestChainWatcherLocalForceCloseDetect(t *testing.T) { |
|
t.Parallel() |
|
|
|
// localForceCloseScenario is the primary test we'll use to execute our |
|
// table driven tests. We'll assert that for any number of state |
|
// updates, and if the commitment transaction has our output or not, |
|
// we're able to properly detect a local force close. |
|
localForceCloseScenario := func(t *testing.T, numUpdates, localState uint8, |
|
remoteOutputOnly, localOutputOnly bool) bool { |
|
|
|
// First, we'll create two channels which already have |
|
// established a commitment contract between themselves. |
|
aliceChannel, bobChannel, cleanUp, err := lnwallet.CreateTestChannels( |
|
channeldb.SingleFunderBit, |
|
) |
|
if err != nil { |
|
t.Fatalf("unable to create test channels: %v", err) |
|
} |
|
defer cleanUp() |
|
|
|
// We'll execute a number of state transitions based on the |
|
// randomly selected number from testing/quick. We do this to |
|
// get more coverage of various state hint encodings beyond 0 |
|
// and 1. |
|
const htlcAmt = 1000 |
|
states, cleanStates, err := executeStateTransitions( |
|
t, htlcAmt, aliceChannel, bobChannel, numUpdates, |
|
) |
|
if err != nil { |
|
t.Errorf("unable to trigger state "+ |
|
"transition: %v", err) |
|
return false |
|
} |
|
defer cleanStates() |
|
|
|
// We'll use the state this test case wants Alice to start at. |
|
aliceChanState := states[localState] |
|
|
|
// With the channels created, we'll now create a chain watcher |
|
// instance which will be watching for any closes of Alice's |
|
// channel. |
|
aliceNotifier := &mock.ChainNotifier{ |
|
SpendChan: make(chan *chainntnfs.SpendDetail), |
|
EpochChan: make(chan *chainntnfs.BlockEpoch), |
|
ConfChan: make(chan *chainntnfs.TxConfirmation), |
|
} |
|
aliceChainWatcher, err := newChainWatcher(chainWatcherConfig{ |
|
chanState: aliceChanState, |
|
notifier: aliceNotifier, |
|
signer: aliceChannel.Signer, |
|
extractStateNumHint: lnwallet.GetStateNumHint, |
|
}) |
|
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 so we can be notified of our fake close below. |
|
chanEvents := aliceChainWatcher.SubscribeChannelEvents() |
|
|
|
// Next, we'll obtain Alice's commitment transaction and |
|
// trigger a force close. This should cause her to detect a |
|
// local force close, and dispatch a local close event. |
|
aliceCommit := aliceChannel.State().LocalCommitment.CommitTx |
|
|
|
// Since this is Alice's commitment, her output is always first |
|
// since she's the one creating the HTLCs (lower balance). In |
|
// order to simulate the commitment only having the remote |
|
// party's output, we'll remove Alice's output. |
|
if remoteOutputOnly { |
|
aliceCommit.TxOut = aliceCommit.TxOut[1:] |
|
} |
|
if localOutputOnly { |
|
aliceCommit.TxOut = aliceCommit.TxOut[:1] |
|
} |
|
|
|
aliceTxHash := aliceCommit.TxHash() |
|
aliceSpend := &chainntnfs.SpendDetail{ |
|
SpenderTxHash: &aliceTxHash, |
|
SpendingTx: aliceCommit, |
|
} |
|
aliceNotifier.SpendChan <- aliceSpend |
|
|
|
// We should get a local force close event from Alice as she |
|
// should be able to detect the close based on the commitment |
|
// outputs. |
|
select { |
|
case summary := <-chanEvents.LocalUnilateralClosure: |
|
// Make sure we correctly extracted the commit |
|
// resolution if we had a local output. |
|
if remoteOutputOnly { |
|
if summary.CommitResolution != nil { |
|
t.Fatalf("expected no commit resolution") |
|
} |
|
} else { |
|
if summary.CommitResolution == nil { |
|
t.Fatalf("expected commit resolution") |
|
} |
|
} |
|
|
|
return true |
|
|
|
case <-time.After(time.Second * 5): |
|
t.Errorf("didn't get local for close for state #%v", |
|
numUpdates) |
|
return false |
|
} |
|
} |
|
|
|
// For our test cases, we'll ensure that we test having a remote output |
|
// present and absent with non or some number of updates in the channel. |
|
testCases := []struct { |
|
numUpdates uint8 |
|
localState uint8 |
|
remoteOutputOnly bool |
|
localOutputOnly bool |
|
}{ |
|
{ |
|
numUpdates: 0, |
|
localState: 0, |
|
remoteOutputOnly: true, |
|
}, |
|
{ |
|
numUpdates: 0, |
|
localState: 0, |
|
remoteOutputOnly: false, |
|
}, |
|
{ |
|
numUpdates: 0, |
|
localState: 0, |
|
localOutputOnly: true, |
|
}, |
|
{ |
|
numUpdates: 20, |
|
localState: 20, |
|
remoteOutputOnly: false, |
|
}, |
|
{ |
|
numUpdates: 20, |
|
localState: 20, |
|
remoteOutputOnly: true, |
|
}, |
|
{ |
|
numUpdates: 20, |
|
localState: 20, |
|
localOutputOnly: true, |
|
}, |
|
{ |
|
numUpdates: 20, |
|
localState: 5, |
|
remoteOutputOnly: false, |
|
}, |
|
{ |
|
numUpdates: 20, |
|
localState: 5, |
|
remoteOutputOnly: true, |
|
}, |
|
{ |
|
numUpdates: 20, |
|
localState: 5, |
|
localOutputOnly: true, |
|
}, |
|
} |
|
for _, testCase := range testCases { |
|
testName := fmt.Sprintf( |
|
"num_updates=%v,remote_output=%v,local_output=%v", |
|
testCase.numUpdates, testCase.remoteOutputOnly, |
|
testCase.localOutputOnly, |
|
) |
|
|
|
testCase := testCase |
|
t.Run(testName, func(t *testing.T) { |
|
t.Parallel() |
|
|
|
localForceCloseScenario( |
|
t, testCase.numUpdates, testCase.localState, |
|
testCase.remoteOutputOnly, |
|
testCase.localOutputOnly, |
|
) |
|
}) |
|
} |
|
}
|
|
|