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.
1315 lines
35 KiB
1315 lines
35 KiB
package contractcourt |
|
|
|
import ( |
|
"bytes" |
|
"fmt" |
|
"reflect" |
|
"sync" |
|
"testing" |
|
"time" |
|
|
|
"github.com/btcsuite/btcd/chaincfg/chainhash" |
|
"github.com/btcsuite/btcd/txscript" |
|
"github.com/btcsuite/btcd/wire" |
|
"github.com/btcsuite/btcutil" |
|
"github.com/lightningnetwork/lnd/chainntnfs" |
|
"github.com/lightningnetwork/lnd/channeldb" |
|
"github.com/lightningnetwork/lnd/input" |
|
"github.com/lightningnetwork/lnd/kvdb" |
|
"github.com/lightningnetwork/lnd/lntest/mock" |
|
"github.com/lightningnetwork/lnd/lntypes" |
|
"github.com/lightningnetwork/lnd/lnwallet" |
|
"github.com/stretchr/testify/require" |
|
) |
|
|
|
type mockWitnessBeacon struct { |
|
preImageUpdates chan lntypes.Preimage |
|
newPreimages chan []lntypes.Preimage |
|
lookupPreimage map[lntypes.Hash]lntypes.Preimage |
|
} |
|
|
|
func newMockWitnessBeacon() *mockWitnessBeacon { |
|
return &mockWitnessBeacon{ |
|
preImageUpdates: make(chan lntypes.Preimage, 1), |
|
newPreimages: make(chan []lntypes.Preimage), |
|
lookupPreimage: make(map[lntypes.Hash]lntypes.Preimage), |
|
} |
|
} |
|
|
|
func (m *mockWitnessBeacon) SubscribeUpdates() *WitnessSubscription { |
|
return &WitnessSubscription{ |
|
WitnessUpdates: m.preImageUpdates, |
|
CancelSubscription: func() {}, |
|
} |
|
} |
|
|
|
func (m *mockWitnessBeacon) LookupPreimage(payhash lntypes.Hash) (lntypes.Preimage, bool) { |
|
preimage, ok := m.lookupPreimage[payhash] |
|
if !ok { |
|
return lntypes.Preimage{}, false |
|
} |
|
return preimage, true |
|
} |
|
|
|
func (m *mockWitnessBeacon) AddPreimages(preimages ...lntypes.Preimage) error { |
|
m.newPreimages <- preimages |
|
return nil |
|
} |
|
|
|
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all |
|
// variations of possible local+remote spends. |
|
func TestHtlcTimeoutResolver(t *testing.T) { |
|
t.Parallel() |
|
|
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) |
|
|
|
var ( |
|
htlcOutpoint wire.OutPoint |
|
fakePreimage lntypes.Preimage |
|
) |
|
fakeSignDesc := &input.SignDescriptor{ |
|
Output: &wire.TxOut{}, |
|
} |
|
|
|
copy(fakePreimage[:], fakePreimageBytes) |
|
|
|
signer := &mock.DummySigner{} |
|
sweepTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: htlcOutpoint, |
|
Witness: [][]byte{{0x01}}, |
|
}, |
|
}, |
|
} |
|
fakeTimeout := int32(5) |
|
|
|
templateTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: htlcOutpoint, |
|
}, |
|
}, |
|
} |
|
|
|
testCases := []struct { |
|
// name is a human readable description of the test case. |
|
name string |
|
|
|
// remoteCommit denotes if the commitment broadcast was the |
|
// remote commitment or not. |
|
remoteCommit bool |
|
|
|
// timeout denotes if the HTLC should be let timeout, or if the |
|
// "remote" party should sweep it on-chain. This also affects |
|
// what type of resolution message we expect. |
|
timeout bool |
|
|
|
// txToBroadcast is a function closure that should generate the |
|
// transaction that should spend the HTLC output. Test authors |
|
// can use this to customize the witness used when spending to |
|
// trigger various redemption cases. |
|
txToBroadcast func() (*wire.MsgTx, error) |
|
|
|
// outcome is the resolver outcome that we expect to be reported |
|
// once the contract is fully resolved. |
|
outcome channeldb.ResolverOutcome |
|
}{ |
|
// Remote commitment is broadcast, we time out the HTLC on |
|
// chain, and should expect a fail HTLC resolution. |
|
{ |
|
name: "timeout remote tx", |
|
remoteCommit: true, |
|
timeout: true, |
|
txToBroadcast: func() (*wire.MsgTx, error) { |
|
witness, err := input.ReceiverHtlcSpendTimeout( |
|
signer, fakeSignDesc, sweepTx, |
|
fakeTimeout, |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// To avoid triggering the race detector by |
|
// setting the witness the second time this |
|
// method is called during tests, we return |
|
// immediately if the witness is already set |
|
// correctly. |
|
if reflect.DeepEqual( |
|
templateTx.TxIn[0].Witness, witness, |
|
) { |
|
return templateTx, nil |
|
} |
|
templateTx.TxIn[0].Witness = witness |
|
return templateTx, nil |
|
}, |
|
outcome: channeldb.ResolverOutcomeTimeout, |
|
}, |
|
|
|
// Our local commitment is broadcast, we timeout the HTLC and |
|
// still expect an HTLC fail resolution. |
|
{ |
|
name: "timeout local tx", |
|
remoteCommit: false, |
|
timeout: true, |
|
txToBroadcast: func() (*wire.MsgTx, error) { |
|
witness, err := input.SenderHtlcSpendTimeout( |
|
&mock.DummySignature{}, txscript.SigHashAll, |
|
signer, fakeSignDesc, sweepTx, |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// To avoid triggering the race detector by |
|
// setting the witness the second time this |
|
// method is called during tests, we return |
|
// immediately if the witness is already set |
|
// correctly. |
|
if reflect.DeepEqual( |
|
templateTx.TxIn[0].Witness, witness, |
|
) { |
|
return templateTx, nil |
|
} |
|
|
|
templateTx.TxIn[0].Witness = witness |
|
|
|
// Set the outpoint to be on our commitment, since |
|
// we need to claim in two stages. |
|
templateTx.TxIn[0].PreviousOutPoint = testChanPoint1 |
|
return templateTx, nil |
|
}, |
|
outcome: channeldb.ResolverOutcomeTimeout, |
|
}, |
|
|
|
// The remote commitment is broadcast, they sweep with the |
|
// pre-image, we should get a settle HTLC resolution. |
|
{ |
|
name: "success remote tx", |
|
remoteCommit: true, |
|
timeout: false, |
|
txToBroadcast: func() (*wire.MsgTx, error) { |
|
witness, err := input.ReceiverHtlcSpendRedeem( |
|
&mock.DummySignature{}, txscript.SigHashAll, |
|
fakePreimageBytes, signer, fakeSignDesc, |
|
sweepTx, |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// To avoid triggering the race detector by |
|
// setting the witness the second time this |
|
// method is called during tests, we return |
|
// immediately if the witness is already set |
|
// correctly. |
|
if reflect.DeepEqual( |
|
templateTx.TxIn[0].Witness, witness, |
|
) { |
|
return templateTx, nil |
|
} |
|
|
|
templateTx.TxIn[0].Witness = witness |
|
return templateTx, nil |
|
}, |
|
outcome: channeldb.ResolverOutcomeClaimed, |
|
}, |
|
|
|
// The local commitment is broadcast, they sweep it with a |
|
// timeout from the output, and we should still get the HTLC |
|
// settle resolution back. |
|
{ |
|
name: "success local tx", |
|
remoteCommit: false, |
|
timeout: false, |
|
txToBroadcast: func() (*wire.MsgTx, error) { |
|
witness, err := input.SenderHtlcSpendRedeem( |
|
signer, fakeSignDesc, sweepTx, |
|
fakePreimageBytes, |
|
) |
|
if err != nil { |
|
return nil, err |
|
} |
|
|
|
// To avoid triggering the race detector by |
|
// setting the witness the second time this |
|
// method is called during tests, we return |
|
// immediately if the witness is already set |
|
// correctly. |
|
if reflect.DeepEqual( |
|
templateTx.TxIn[0].Witness, witness, |
|
) { |
|
return templateTx, nil |
|
} |
|
|
|
templateTx.TxIn[0].Witness = witness |
|
return templateTx, nil |
|
}, |
|
outcome: channeldb.ResolverOutcomeClaimed, |
|
}, |
|
} |
|
|
|
notifier := &mock.ChainNotifier{ |
|
EpochChan: make(chan *chainntnfs.BlockEpoch), |
|
SpendChan: make(chan *chainntnfs.SpendDetail), |
|
ConfChan: make(chan *chainntnfs.TxConfirmation), |
|
} |
|
witnessBeacon := newMockWitnessBeacon() |
|
|
|
for _, testCase := range testCases { |
|
t.Logf("Running test case: %v", testCase.name) |
|
|
|
checkPointChan := make(chan struct{}, 1) |
|
incubateChan := make(chan struct{}, 1) |
|
resolutionChan := make(chan ResolutionMsg, 1) |
|
reportChan := make(chan *channeldb.ResolverReport) |
|
|
|
chainCfg := ChannelArbitratorConfig{ |
|
ChainArbitratorConfig: ChainArbitratorConfig{ |
|
Notifier: notifier, |
|
PreimageDB: witnessBeacon, |
|
IncubateOutputs: func(wire.OutPoint, |
|
*lnwallet.OutgoingHtlcResolution, |
|
*lnwallet.IncomingHtlcResolution, |
|
uint32) error { |
|
|
|
incubateChan <- struct{}{} |
|
return nil |
|
}, |
|
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error { |
|
if len(msgs) != 1 { |
|
return fmt.Errorf("expected 1 "+ |
|
"resolution msg, instead got %v", |
|
len(msgs)) |
|
} |
|
|
|
resolutionChan <- msgs[0] |
|
return nil |
|
}, |
|
}, |
|
PutResolverReport: func(_ kvdb.RwTx, |
|
_ *channeldb.ResolverReport) error { |
|
|
|
return nil |
|
}, |
|
} |
|
|
|
cfg := ResolverConfig{ |
|
ChannelArbitratorConfig: chainCfg, |
|
Checkpoint: func(_ ContractResolver, |
|
reports ...*channeldb.ResolverReport) error { |
|
|
|
checkPointChan <- struct{}{} |
|
|
|
// Send all of our reports into the channel. |
|
for _, report := range reports { |
|
reportChan <- report |
|
} |
|
|
|
return nil |
|
}, |
|
} |
|
resolver := &htlcTimeoutResolver{ |
|
htlcResolution: lnwallet.OutgoingHtlcResolution{ |
|
ClaimOutpoint: testChanPoint2, |
|
SweepSignDesc: *fakeSignDesc, |
|
}, |
|
contractResolverKit: *newContractResolverKit( |
|
cfg, |
|
), |
|
htlc: channeldb.HTLC{ |
|
Amt: testHtlcAmt, |
|
}, |
|
} |
|
|
|
var reports []*channeldb.ResolverReport |
|
|
|
// If the test case needs the remote commitment to be |
|
// broadcast, then we'll set the timeout commit to a fake |
|
// transaction to force the code path. |
|
if !testCase.remoteCommit { |
|
timeoutTx, err := testCase.txToBroadcast() |
|
require.NoError(t, err) |
|
|
|
resolver.htlcResolution.SignedTimeoutTx = timeoutTx |
|
|
|
if testCase.timeout { |
|
timeoutTxID := timeoutTx.TxHash() |
|
reports = append(reports, &channeldb.ResolverReport{ |
|
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, |
|
Amount: testHtlcAmt.ToSatoshis(), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, |
|
SpendTxID: &timeoutTxID, |
|
}) |
|
} |
|
} |
|
|
|
// With all the setup above complete, we can initiate the |
|
// resolution process, and the bulk of our test. |
|
var wg sync.WaitGroup |
|
resolveErr := make(chan error, 1) |
|
wg.Add(1) |
|
go func() { |
|
defer wg.Done() |
|
|
|
_, err := resolver.Resolve() |
|
if err != nil { |
|
resolveErr <- err |
|
} |
|
}() |
|
|
|
// At the output isn't yet in the nursery, we expect that we |
|
// should receive an incubation request. |
|
select { |
|
case <-incubateChan: |
|
case err := <-resolveErr: |
|
t.Fatalf("unable to resolve HTLC: %v", err) |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("failed to receive incubation request") |
|
} |
|
|
|
// Next, the resolver should request a spend notification for |
|
// the direct HTLC output. We'll use the txToBroadcast closure |
|
// for the test case to generate the transaction that we'll |
|
// send to the resolver. |
|
spendingTx, err := testCase.txToBroadcast() |
|
if err != nil { |
|
t.Fatalf("unable to generate tx: %v", err) |
|
} |
|
spendTxHash := spendingTx.TxHash() |
|
|
|
select { |
|
case notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: spendingTx, |
|
SpenderTxHash: &spendTxHash, |
|
}: |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("failed to request spend ntfn") |
|
} |
|
|
|
if !testCase.timeout { |
|
// If the resolver should settle now, then we'll |
|
// extract the pre-image to be extracted and the |
|
// resolution message sent. |
|
select { |
|
case newPreimage := <-witnessBeacon.newPreimages: |
|
if newPreimage[0] != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, newPreimage) |
|
} |
|
|
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("pre-image not added") |
|
} |
|
|
|
// Finally, we should get a resolution message with the |
|
// pre-image set within the message. |
|
select { |
|
case resolutionMsg := <-resolutionChan: |
|
// Once again, the pre-images should match up. |
|
if *resolutionMsg.PreImage != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, resolutionMsg.PreImage) |
|
} |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("resolution not sent") |
|
} |
|
} else { |
|
|
|
// Otherwise, the HTLC should now timeout. First, we |
|
// should get a resolution message with a populated |
|
// failure message. |
|
select { |
|
case resolutionMsg := <-resolutionChan: |
|
if resolutionMsg.Failure == nil { |
|
t.Fatalf("expected failure resolution msg") |
|
} |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("resolution not sent") |
|
} |
|
|
|
// We should also get another request for the spend |
|
// notification of the second-level transaction to |
|
// indicate that it's been swept by the nursery, but |
|
// only if this is a local commitment transaction. |
|
if !testCase.remoteCommit { |
|
select { |
|
case notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: spendingTx, |
|
SpenderTxHash: &spendTxHash, |
|
}: |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("failed to request spend ntfn") |
|
} |
|
} |
|
} |
|
|
|
// In any case, before the resolver exits, it should checkpoint |
|
// its final state. |
|
select { |
|
case <-checkPointChan: |
|
case err := <-resolveErr: |
|
t.Fatalf("unable to resolve HTLC: %v", err) |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("check point not received") |
|
} |
|
|
|
// Add a report to our set of expected reports with the outcome |
|
// that the test specifies (either success or timeout). |
|
spendTxID := spendingTx.TxHash() |
|
amt := btcutil.Amount(fakeSignDesc.Output.Value) |
|
|
|
reports = append(reports, &channeldb.ResolverReport{ |
|
OutPoint: testChanPoint2, |
|
Amount: amt, |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: testCase.outcome, |
|
SpendTxID: &spendTxID, |
|
}) |
|
|
|
for _, report := range reports { |
|
assertResolverReport(t, reportChan, report) |
|
} |
|
|
|
wg.Wait() |
|
|
|
// Finally, the resolver should be marked as resolved. |
|
if !resolver.resolved { |
|
t.Fatalf("resolver should be marked as resolved") |
|
} |
|
} |
|
} |
|
|
|
// NOTE: the following tests essentially checks many of the same scenarios as |
|
// the test above, but they expand on it by checking resuming from checkpoints |
|
// at every stage. |
|
|
|
// TestHtlcTimeoutSingleStage tests a remote commitment confirming, and the |
|
// local node sweeping the HTLC output directly after timeout. |
|
func TestHtlcTimeoutSingleStage(t *testing.T) { |
|
commitOutpoint := wire.OutPoint{Index: 3} |
|
|
|
sweepTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{{}}, |
|
TxOut: []*wire.TxOut{{}}, |
|
} |
|
|
|
// singleStageResolution is a resolution for a htlc on the remote |
|
// party's commitment. |
|
singleStageResolution := lnwallet.OutgoingHtlcResolution{ |
|
ClaimOutpoint: commitOutpoint, |
|
SweepSignDesc: testSignDesc, |
|
} |
|
|
|
sweepTxid := sweepTx.TxHash() |
|
claim := &channeldb.ResolverReport{ |
|
OutPoint: commitOutpoint, |
|
Amount: btcutil.Amount(testSignDesc.Output.Value), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout, |
|
SpendTxID: &sweepTxid, |
|
} |
|
|
|
checkpoints := []checkpoint{ |
|
{ |
|
// Output should be handed off to the nursery. |
|
incubating: true, |
|
}, |
|
{ |
|
// We send a confirmation the sweep tx from published |
|
// by the nursery. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
_ bool) error { |
|
// The nursery will create and publish a sweep |
|
// tx. |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: sweepTx, |
|
SpenderTxHash: &sweepTxid, |
|
} |
|
|
|
// The resolver should deliver a failure |
|
// resolition message (indicating we |
|
// successfully timed out the HTLC). |
|
select { |
|
case resolutionMsg := <-ctx.resolutionChan: |
|
if resolutionMsg.Failure == nil { |
|
t.Fatalf("expected failure resolution msg") |
|
} |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("resolution not sent") |
|
} |
|
|
|
return nil |
|
}, |
|
|
|
// After the sweep has confirmed, we expect the |
|
// checkpoint to be resolved, and with the above |
|
// report. |
|
incubating: true, |
|
resolved: true, |
|
reports: []*channeldb.ResolverReport{ |
|
claim, |
|
}, |
|
}, |
|
} |
|
|
|
testHtlcTimeout( |
|
t, singleStageResolution, checkpoints, |
|
) |
|
} |
|
|
|
// TestHtlcTimeoutSecondStage tests a local commitment being confirmed, and the |
|
// local node claiming the HTLC output using the second-level timeout tx. |
|
func TestHtlcTimeoutSecondStage(t *testing.T) { |
|
commitOutpoint := wire.OutPoint{Index: 2} |
|
htlcOutpoint := wire.OutPoint{Index: 3} |
|
|
|
sweepTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{{}}, |
|
TxOut: []*wire.TxOut{{}}, |
|
} |
|
sweepHash := sweepTx.TxHash() |
|
|
|
timeoutTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: commitOutpoint, |
|
}, |
|
}, |
|
TxOut: []*wire.TxOut{ |
|
{ |
|
Value: 111, |
|
PkScript: []byte{0xaa, 0xaa}, |
|
}, |
|
}, |
|
} |
|
|
|
signer := &mock.DummySigner{} |
|
witness, err := input.SenderHtlcSpendTimeout( |
|
&mock.DummySignature{}, txscript.SigHashAll, |
|
signer, &testSignDesc, timeoutTx, |
|
) |
|
require.NoError(t, err) |
|
timeoutTx.TxIn[0].Witness = witness |
|
|
|
timeoutTxid := timeoutTx.TxHash() |
|
|
|
// twoStageResolution is a resolution for a htlc on the local |
|
// party's commitment. |
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{ |
|
ClaimOutpoint: htlcOutpoint, |
|
SignedTimeoutTx: timeoutTx, |
|
SweepSignDesc: testSignDesc, |
|
} |
|
|
|
firstStage := &channeldb.ResolverReport{ |
|
OutPoint: commitOutpoint, |
|
Amount: testHtlcAmt.ToSatoshis(), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, |
|
SpendTxID: &timeoutTxid, |
|
} |
|
|
|
secondState := &channeldb.ResolverReport{ |
|
OutPoint: htlcOutpoint, |
|
Amount: btcutil.Amount(testSignDesc.Output.Value), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout, |
|
SpendTxID: &sweepHash, |
|
} |
|
|
|
checkpoints := []checkpoint{ |
|
{ |
|
// Output should be handed off to the nursery. |
|
incubating: true, |
|
}, |
|
{ |
|
// We send a confirmation for our sweep tx to indicate |
|
// that our sweep succeeded. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
_ bool) error { |
|
// The nursery will publish the timeout tx. |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: timeoutTx, |
|
SpenderTxHash: &timeoutTxid, |
|
} |
|
|
|
// The resolver should deliver a failure |
|
// resolution message (indicating we |
|
// successfully timed out the HTLC). |
|
select { |
|
case resolutionMsg := <-ctx.resolutionChan: |
|
if resolutionMsg.Failure == nil { |
|
t.Fatalf("expected failure resolution msg") |
|
} |
|
case <-time.After(time.Second * 1): |
|
t.Fatalf("resolution not sent") |
|
} |
|
|
|
// Deliver spend of timeout tx. |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: sweepTx, |
|
SpenderTxHash: &sweepHash, |
|
} |
|
|
|
return nil |
|
}, |
|
|
|
// After the sweep has confirmed, we expect the |
|
// checkpoint to be resolved, and with the above |
|
// reports. |
|
incubating: true, |
|
resolved: true, |
|
reports: []*channeldb.ResolverReport{ |
|
firstStage, secondState, |
|
}, |
|
}, |
|
} |
|
|
|
testHtlcTimeout( |
|
t, twoStageResolution, checkpoints, |
|
) |
|
} |
|
|
|
// TestHtlcTimeoutSingleStageRemoteSpend tests that when a local commitment |
|
// confirms, and the remote spends the HTLC output directly, we detect this and |
|
// extract the preimage. |
|
func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) { |
|
commitOutpoint := wire.OutPoint{Index: 2} |
|
htlcOutpoint := wire.OutPoint{Index: 3} |
|
|
|
spendTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{{}}, |
|
TxOut: []*wire.TxOut{{}}, |
|
} |
|
|
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) |
|
var fakePreimage lntypes.Preimage |
|
copy(fakePreimage[:], fakePreimageBytes) |
|
|
|
signer := &mock.DummySigner{} |
|
witness, err := input.SenderHtlcSpendRedeem( |
|
signer, &testSignDesc, spendTx, |
|
fakePreimageBytes, |
|
) |
|
require.NoError(t, err) |
|
spendTx.TxIn[0].Witness = witness |
|
|
|
spendTxHash := spendTx.TxHash() |
|
|
|
timeoutTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: commitOutpoint, |
|
}, |
|
}, |
|
TxOut: []*wire.TxOut{ |
|
{ |
|
Value: 123, |
|
PkScript: []byte{0xff, 0xff}, |
|
}, |
|
}, |
|
} |
|
|
|
timeoutWitness, err := input.SenderHtlcSpendTimeout( |
|
&mock.DummySignature{}, txscript.SigHashAll, |
|
signer, &testSignDesc, timeoutTx, |
|
) |
|
require.NoError(t, err) |
|
timeoutTx.TxIn[0].Witness = timeoutWitness |
|
|
|
// twoStageResolution is a resolution for a htlc on the local |
|
// party's commitment. |
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{ |
|
ClaimOutpoint: htlcOutpoint, |
|
SignedTimeoutTx: timeoutTx, |
|
SweepSignDesc: testSignDesc, |
|
} |
|
|
|
claim := &channeldb.ResolverReport{ |
|
OutPoint: htlcOutpoint, |
|
Amount: btcutil.Amount(testSignDesc.Output.Value), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed, |
|
SpendTxID: &spendTxHash, |
|
} |
|
|
|
checkpoints := []checkpoint{ |
|
{ |
|
// Output should be handed off to the nursery. |
|
incubating: true, |
|
}, |
|
{ |
|
// We send a spend notification for a remote spend with |
|
// the preimage. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
_ bool) error { |
|
|
|
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon) |
|
|
|
// The remote spends the output direcly with |
|
// the preimage. |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: spendTx, |
|
SpenderTxHash: &spendTxHash, |
|
} |
|
|
|
// We should extract the preimage. |
|
select { |
|
case newPreimage := <-witnessBeacon.newPreimages: |
|
if newPreimage[0] != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, newPreimage) |
|
} |
|
|
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("pre-image not added") |
|
} |
|
|
|
// Finally, we should get a resolution message |
|
// with the pre-image set within the message. |
|
select { |
|
case resolutionMsg := <-ctx.resolutionChan: |
|
if *resolutionMsg.PreImage != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, resolutionMsg.PreImage) |
|
} |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("resolution not sent") |
|
} |
|
|
|
return nil |
|
}, |
|
|
|
// After the success tx has confirmed, we expect the |
|
// checkpoint to be resolved, and with the above |
|
// report. |
|
incubating: true, |
|
resolved: true, |
|
reports: []*channeldb.ResolverReport{ |
|
claim, |
|
}, |
|
}, |
|
} |
|
|
|
testHtlcTimeout( |
|
t, twoStageResolution, checkpoints, |
|
) |
|
} |
|
|
|
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment |
|
// confirms, and the remote spends the output using the success tx, we |
|
// properly detect this and extract the preimage. |
|
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) { |
|
commitOutpoint := wire.OutPoint{Index: 2} |
|
|
|
remoteSuccessTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: commitOutpoint, |
|
}, |
|
}, |
|
TxOut: []*wire.TxOut{}, |
|
} |
|
|
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) |
|
var fakePreimage lntypes.Preimage |
|
copy(fakePreimage[:], fakePreimageBytes) |
|
|
|
signer := &mock.DummySigner{} |
|
witness, err := input.ReceiverHtlcSpendRedeem( |
|
&mock.DummySignature{}, txscript.SigHashAll, |
|
fakePreimageBytes, signer, |
|
&testSignDesc, remoteSuccessTx, |
|
) |
|
require.NoError(t, err) |
|
|
|
remoteSuccessTx.TxIn[0].Witness = witness |
|
successTxid := remoteSuccessTx.TxHash() |
|
|
|
// singleStageResolution allwoing the local node to sweep HTLC output |
|
// directly from the remote commitment after timeout. |
|
singleStageResolution := lnwallet.OutgoingHtlcResolution{ |
|
ClaimOutpoint: commitOutpoint, |
|
SweepSignDesc: testSignDesc, |
|
} |
|
|
|
claim := &channeldb.ResolverReport{ |
|
OutPoint: commitOutpoint, |
|
Amount: btcutil.Amount(testSignDesc.Output.Value), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed, |
|
SpendTxID: &successTxid, |
|
} |
|
|
|
checkpoints := []checkpoint{ |
|
{ |
|
// Output should be handed off to the nursery. |
|
incubating: true, |
|
}, |
|
{ |
|
// We send a confirmation for the remote's second layer |
|
// success transcation. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
_ bool) error { |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: remoteSuccessTx, |
|
SpenderTxHash: &successTxid, |
|
} |
|
|
|
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon) |
|
|
|
// We expect the preimage to be extracted, |
|
select { |
|
case newPreimage := <-witnessBeacon.newPreimages: |
|
if newPreimage[0] != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, newPreimage) |
|
} |
|
|
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("pre-image not added") |
|
} |
|
|
|
// Finally, we should get a resolution message with the |
|
// pre-image set within the message. |
|
select { |
|
case resolutionMsg := <-ctx.resolutionChan: |
|
if *resolutionMsg.PreImage != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, resolutionMsg.PreImage) |
|
} |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("resolution not sent") |
|
} |
|
|
|
return nil |
|
}, |
|
|
|
// After the sweep has confirmed, we expect the |
|
// checkpoint to be resolved, and with the above |
|
// report. |
|
incubating: true, |
|
resolved: true, |
|
reports: []*channeldb.ResolverReport{ |
|
claim, |
|
}, |
|
}, |
|
} |
|
|
|
testHtlcTimeout( |
|
t, singleStageResolution, checkpoints, |
|
) |
|
} |
|
|
|
// TestHtlcTimeoutSecondStageSweeper tests that for anchor channels, when a |
|
// local commitment confirms, the timeout tx is handed to the sweeper to claim |
|
// the HTLC output. |
|
func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { |
|
commitOutpoint := wire.OutPoint{Index: 2} |
|
htlcOutpoint := wire.OutPoint{Index: 3} |
|
|
|
sweepTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{{}}, |
|
TxOut: []*wire.TxOut{{}}, |
|
} |
|
sweepHash := sweepTx.TxHash() |
|
|
|
timeoutTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: commitOutpoint, |
|
}, |
|
}, |
|
TxOut: []*wire.TxOut{ |
|
{ |
|
Value: 123, |
|
PkScript: []byte{0xff, 0xff}, |
|
}, |
|
}, |
|
} |
|
|
|
// We set the timeout witness since the script is used when subscribing |
|
// to spends. |
|
signer := &mock.DummySigner{} |
|
timeoutWitness, err := input.SenderHtlcSpendTimeout( |
|
&mock.DummySignature{}, txscript.SigHashAll, |
|
signer, &testSignDesc, timeoutTx, |
|
) |
|
require.NoError(t, err) |
|
timeoutTx.TxIn[0].Witness = timeoutWitness |
|
|
|
reSignedTimeoutTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: wire.OutPoint{ |
|
Hash: chainhash.Hash{0xaa, 0xbb}, |
|
Index: 0, |
|
}, |
|
}, |
|
timeoutTx.TxIn[0], |
|
{ |
|
PreviousOutPoint: wire.OutPoint{ |
|
Hash: chainhash.Hash{0xaa, 0xbb}, |
|
Index: 2, |
|
}, |
|
}, |
|
}, |
|
|
|
TxOut: []*wire.TxOut{ |
|
{ |
|
Value: 111, |
|
PkScript: []byte{0xaa, 0xaa}, |
|
}, |
|
timeoutTx.TxOut[0], |
|
}, |
|
} |
|
reSignedHash := reSignedTimeoutTx.TxHash() |
|
reSignedOutPoint := wire.OutPoint{ |
|
Hash: reSignedHash, |
|
Index: 1, |
|
} |
|
|
|
// twoStageResolution is a resolution for a htlc on the local |
|
// party's commitment, where the timout tx can be re-signed. |
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{ |
|
ClaimOutpoint: htlcOutpoint, |
|
SignedTimeoutTx: timeoutTx, |
|
SignDetails: &input.SignDetails{ |
|
SignDesc: testSignDesc, |
|
PeerSig: testSig, |
|
}, |
|
SweepSignDesc: testSignDesc, |
|
} |
|
|
|
firstStage := &channeldb.ResolverReport{ |
|
OutPoint: commitOutpoint, |
|
Amount: testHtlcAmt.ToSatoshis(), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, |
|
SpendTxID: &reSignedHash, |
|
} |
|
|
|
secondState := &channeldb.ResolverReport{ |
|
OutPoint: reSignedOutPoint, |
|
Amount: btcutil.Amount(testSignDesc.Output.Value), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout, |
|
SpendTxID: &sweepHash, |
|
} |
|
|
|
checkpoints := []checkpoint{ |
|
{ |
|
// The output should be given to the sweeper. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
_ bool) error { |
|
|
|
resolver := ctx.resolver.(*htlcTimeoutResolver) |
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs |
|
op := inp.OutPoint() |
|
if *op != commitOutpoint { |
|
return fmt.Errorf("outpoint %v swept, "+ |
|
"expected %v", op, |
|
commitOutpoint) |
|
} |
|
|
|
// Emulat the sweeper spending using the |
|
// re-signed timeout tx. |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: reSignedTimeoutTx, |
|
SpenderInputIndex: 1, |
|
SpenderTxHash: &reSignedHash, |
|
SpendingHeight: 10, |
|
} |
|
|
|
return nil |
|
}, |
|
// incubating=true is used to signal that the |
|
// second-level transaction was confirmed. |
|
incubating: true, |
|
}, |
|
{ |
|
// We send a confirmation for our sweep tx to indicate |
|
// that our sweep succeeded. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
resumed bool) error { |
|
|
|
// If we are resuming from a checkpoing, we |
|
// expect the resolver to re-subscribe to a |
|
// spend, hence we must resend it. |
|
if resumed { |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: reSignedTimeoutTx, |
|
SpenderInputIndex: 1, |
|
SpenderTxHash: &reSignedHash, |
|
SpendingHeight: 10, |
|
} |
|
} |
|
|
|
// The resolver should deliver a failure |
|
// resolution message (indicating we |
|
// successfully timed out the HTLC). |
|
select { |
|
case resolutionMsg := <-ctx.resolutionChan: |
|
if resolutionMsg.Failure == nil { |
|
t.Fatalf("expected failure resolution msg") |
|
} |
|
case <-time.After(time.Second * 1): |
|
t.Fatalf("resolution not sent") |
|
} |
|
|
|
// Mimic CSV lock expiring. |
|
ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{ |
|
Height: 13, |
|
} |
|
|
|
// The timout tx output should now be given to |
|
// the sweeper. |
|
resolver := ctx.resolver.(*htlcTimeoutResolver) |
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs |
|
op := inp.OutPoint() |
|
exp := wire.OutPoint{ |
|
Hash: reSignedHash, |
|
Index: 1, |
|
} |
|
if *op != exp { |
|
return fmt.Errorf("wrong outpoint swept") |
|
} |
|
|
|
// Notify about the spend, which should resolve |
|
// the resolver. |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: sweepTx, |
|
SpenderTxHash: &sweepHash, |
|
SpendingHeight: 14, |
|
} |
|
|
|
return nil |
|
}, |
|
|
|
// After the sweep has confirmed, we expect the |
|
// checkpoint to be resolved, and with the above |
|
// reports. |
|
incubating: true, |
|
resolved: true, |
|
reports: []*channeldb.ResolverReport{ |
|
firstStage, |
|
secondState, |
|
}, |
|
}, |
|
} |
|
|
|
testHtlcTimeout( |
|
t, twoStageResolution, checkpoints, |
|
) |
|
} |
|
|
|
// TestHtlcTimeoutSecondStageSweeperRemoteSpend tests that if a local timeout |
|
// tx is offered to the sweeper, but the output is swept by the remote node, we |
|
// properly detect this and extract the preimage. |
|
func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) { |
|
commitOutpoint := wire.OutPoint{Index: 2} |
|
htlcOutpoint := wire.OutPoint{Index: 3} |
|
|
|
timeoutTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{ |
|
{ |
|
PreviousOutPoint: commitOutpoint, |
|
}, |
|
}, |
|
TxOut: []*wire.TxOut{ |
|
{ |
|
Value: 123, |
|
PkScript: []byte{0xff, 0xff}, |
|
}, |
|
}, |
|
} |
|
|
|
// We set the timeout witness since the script is used when subscribing |
|
// to spends. |
|
signer := &mock.DummySigner{} |
|
timeoutWitness, err := input.SenderHtlcSpendTimeout( |
|
&mock.DummySignature{}, txscript.SigHashAll, |
|
signer, &testSignDesc, timeoutTx, |
|
) |
|
require.NoError(t, err) |
|
timeoutTx.TxIn[0].Witness = timeoutWitness |
|
|
|
spendTx := &wire.MsgTx{ |
|
TxIn: []*wire.TxIn{{}}, |
|
TxOut: []*wire.TxOut{{}}, |
|
} |
|
|
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) |
|
var fakePreimage lntypes.Preimage |
|
copy(fakePreimage[:], fakePreimageBytes) |
|
|
|
witness, err := input.SenderHtlcSpendRedeem( |
|
signer, &testSignDesc, spendTx, |
|
fakePreimageBytes, |
|
) |
|
require.NoError(t, err) |
|
spendTx.TxIn[0].Witness = witness |
|
|
|
spendTxHash := spendTx.TxHash() |
|
|
|
// twoStageResolution is a resolution for a htlc on the local |
|
// party's commitment, where the timout tx can be re-signed. |
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{ |
|
ClaimOutpoint: htlcOutpoint, |
|
SignedTimeoutTx: timeoutTx, |
|
SignDetails: &input.SignDetails{ |
|
SignDesc: testSignDesc, |
|
PeerSig: testSig, |
|
}, |
|
SweepSignDesc: testSignDesc, |
|
} |
|
|
|
claim := &channeldb.ResolverReport{ |
|
OutPoint: htlcOutpoint, |
|
Amount: btcutil.Amount(testSignDesc.Output.Value), |
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, |
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed, |
|
SpendTxID: &spendTxHash, |
|
} |
|
|
|
checkpoints := []checkpoint{ |
|
{ |
|
// The output should be given to the sweeper. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
_ bool) error { |
|
|
|
resolver := ctx.resolver.(*htlcTimeoutResolver) |
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs |
|
op := inp.OutPoint() |
|
if *op != commitOutpoint { |
|
return fmt.Errorf("outpoint %v swept, "+ |
|
"expected %v", op, |
|
commitOutpoint) |
|
} |
|
|
|
// Emulate the remote sweeping the output with the preimage. |
|
// re-signed timeout tx. |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: spendTx, |
|
SpenderTxHash: &spendTxHash, |
|
} |
|
|
|
return nil |
|
}, |
|
// incubating=true is used to signal that the |
|
// second-level transaction was confirmed. |
|
incubating: true, |
|
}, |
|
{ |
|
// We send a confirmation for our sweep tx to indicate |
|
// that our sweep succeeded. |
|
preCheckpoint: func(ctx *htlcResolverTestContext, |
|
resumed bool) error { |
|
|
|
// If we are resuming from a checkpoing, we |
|
// expect the resolver to re-subscribe to a |
|
// spend, hence we must resend it. |
|
if resumed { |
|
fmt.Println("resumed") |
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ |
|
SpendingTx: spendTx, |
|
SpenderTxHash: &spendTxHash, |
|
} |
|
} |
|
|
|
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon) |
|
|
|
// We should extract the preimage. |
|
select { |
|
case newPreimage := <-witnessBeacon.newPreimages: |
|
if newPreimage[0] != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, newPreimage) |
|
} |
|
|
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("pre-image not added") |
|
} |
|
|
|
// Finally, we should get a resolution message |
|
// with the pre-image set within the message. |
|
select { |
|
case resolutionMsg := <-ctx.resolutionChan: |
|
if *resolutionMsg.PreImage != fakePreimage { |
|
t.Fatalf("wrong pre-image: "+ |
|
"expected %v, got %v", |
|
fakePreimage, resolutionMsg.PreImage) |
|
} |
|
case <-time.After(time.Second * 5): |
|
t.Fatalf("resolution not sent") |
|
} |
|
|
|
return nil |
|
}, |
|
|
|
// After the sweep has confirmed, we expect the |
|
// checkpoint to be resolved, and with the above |
|
// reports. |
|
incubating: true, |
|
resolved: true, |
|
reports: []*channeldb.ResolverReport{ |
|
claim, |
|
}, |
|
}, |
|
} |
|
|
|
testHtlcTimeout( |
|
t, twoStageResolution, checkpoints, |
|
) |
|
} |
|
|
|
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution, |
|
checkpoints []checkpoint) { |
|
|
|
defer timeout(t)() |
|
|
|
// We first run the resolver from start to finish, ensuring it gets |
|
// checkpointed at every expected stage. We store the checkpointed data |
|
// for the next portion of the test. |
|
ctx := newHtlcResolverTestContext(t, |
|
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { |
|
return &htlcTimeoutResolver{ |
|
contractResolverKit: *newContractResolverKit(cfg), |
|
htlc: htlc, |
|
htlcResolution: resolution, |
|
} |
|
}, |
|
) |
|
|
|
checkpointedState := runFromCheckpoint(t, ctx, checkpoints) |
|
|
|
// Now, from every checkpoint created, we re-create the resolver, and |
|
// run the test from that checkpoint. |
|
for i := range checkpointedState { |
|
cp := bytes.NewReader(checkpointedState[i]) |
|
ctx := newHtlcResolverTestContext(t, |
|
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { |
|
resolver, err := newTimeoutResolverFromReader(cp, cfg) |
|
if err != nil { |
|
t.Fatal(err) |
|
} |
|
|
|
resolver.Supplement(htlc) |
|
resolver.htlcResolution = resolution |
|
return resolver |
|
}, |
|
) |
|
|
|
// Run from the given checkpoint, ensuring we'll hit the rest. |
|
_ = runFromCheckpoint(t, ctx, checkpoints[i+1:]) |
|
} |
|
}
|
|
|