contractcourt/htlc_timeout_test: expand timeout tests

This commit is contained in:
Johan T. Halseth 2020-12-09 12:24:03 +01:00
parent 4992e41439
commit bb406c82a9
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
2 changed files with 926 additions and 59 deletions

@ -3,7 +3,6 @@ package contractcourt
import ( import (
"bytes" "bytes"
"fmt" "fmt"
"io"
"reflect" "reflect"
"testing" "testing"
@ -22,30 +21,41 @@ import (
var testHtlcAmt = lnwire.MilliSatoshi(200000) var testHtlcAmt = lnwire.MilliSatoshi(200000)
type htlcSuccessResolverTestContext struct { type htlcResolverTestContext struct {
resolver *htlcSuccessResolver resolver ContractResolver
checkpoint func(_ ContractResolver,
_ ...*channeldb.ResolverReport) error
notifier *mock.ChainNotifier notifier *mock.ChainNotifier
resolverResultChan chan resolveResult resolverResultChan chan resolveResult
t *testing.T resolutionChan chan ResolutionMsg
t *testing.T
} }
func newHtlcSuccessResolverTextContext(t *testing.T, checkpoint io.Reader) *htlcSuccessResolverTestContext { func newHtlcResolverTestContext(t *testing.T,
newResolver func(htlc channeldb.HTLC,
cfg ResolverConfig) ContractResolver) *htlcResolverTestContext {
notifier := &mock.ChainNotifier{ notifier := &mock.ChainNotifier{
EpochChan: make(chan *chainntnfs.BlockEpoch, 1), EpochChan: make(chan *chainntnfs.BlockEpoch, 1),
SpendChan: make(chan *chainntnfs.SpendDetail, 1), SpendChan: make(chan *chainntnfs.SpendDetail, 1),
ConfChan: make(chan *chainntnfs.TxConfirmation, 1), ConfChan: make(chan *chainntnfs.TxConfirmation, 1),
} }
checkPointChan := make(chan struct{}, 1) testCtx := &htlcResolverTestContext{
checkpoint: nil,
testCtx := &htlcSuccessResolverTestContext{ notifier: notifier,
notifier: notifier, resolutionChan: make(chan ResolutionMsg, 1),
t: t, t: t,
} }
witnessBeacon := newMockWitnessBeacon()
chainCfg := ChannelArbitratorConfig{ chainCfg := ChannelArbitratorConfig{
ChainArbitratorConfig: ChainArbitratorConfig{ ChainArbitratorConfig: ChainArbitratorConfig{
Notifier: notifier, Notifier: notifier,
PreimageDB: witnessBeacon,
PublishTx: func(_ *wire.MsgTx, _ string) error { PublishTx: func(_ *wire.MsgTx, _ string) error {
return nil return nil
}, },
@ -54,6 +64,16 @@ func newHtlcSuccessResolverTextContext(t *testing.T, checkpoint io.Reader) *htlc
*lnwallet.IncomingHtlcResolution, uint32) error { *lnwallet.IncomingHtlcResolution, uint32) error {
return nil return nil
}, },
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
if len(msgs) != 1 {
return fmt.Errorf("expected 1 "+
"resolution msg, instead got %v",
len(msgs))
}
testCtx.resolutionChan <- msgs[0]
return nil
},
}, },
PutResolverReport: func(_ kvdb.RwTx, PutResolverReport: func(_ kvdb.RwTx,
report *channeldb.ResolverReport) error { report *channeldb.ResolverReport) error {
@ -61,43 +81,31 @@ func newHtlcSuccessResolverTextContext(t *testing.T, checkpoint io.Reader) *htlc
return nil return nil
}, },
} }
// Since we want to replace this checkpoint method later in the test,
// we wrap the call to it in a closure. The linter will complain about
// this so set nolint directive.
checkpointFunc := func(c ContractResolver, // nolint
r ...*channeldb.ResolverReport) error {
return testCtx.checkpoint(c, r...)
}
cfg := ResolverConfig{ cfg := ResolverConfig{
ChannelArbitratorConfig: chainCfg, ChannelArbitratorConfig: chainCfg,
Checkpoint: func(_ ContractResolver, Checkpoint: checkpointFunc,
_ ...*channeldb.ResolverReport) error {
checkPointChan <- struct{}{}
return nil
},
} }
htlc := channeldb.HTLC{ htlc := channeldb.HTLC{
RHash: testResHash, RHash: testResHash,
OnionBlob: testOnionBlob, OnionBlob: testOnionBlob,
Amt: testHtlcAmt, Amt: testHtlcAmt,
} }
if checkpoint != nil {
var err error
testCtx.resolver, err = newSuccessResolverFromReader(checkpoint, cfg)
if err != nil {
t.Fatal(err)
}
testCtx.resolver.Supplement(htlc) testCtx.resolver = newResolver(htlc, cfg)
} else {
testCtx.resolver = &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlcResolution: lnwallet.IncomingHtlcResolution{},
htlc: htlc,
}
}
return testCtx return testCtx
} }
func (i *htlcSuccessResolverTestContext) resolve() { func (i *htlcResolverTestContext) resolve() {
// Start resolver. // Start resolver.
i.resolverResultChan = make(chan resolveResult, 1) i.resolverResultChan = make(chan resolveResult, 1)
go func() { go func() {
@ -109,7 +117,7 @@ func (i *htlcSuccessResolverTestContext) resolve() {
}() }()
} }
func (i *htlcSuccessResolverTestContext) waitForResult() { func (i *htlcResolverTestContext) waitForResult() {
i.t.Helper() i.t.Helper()
result := <-i.resolverResultChan result := <-i.resolverResultChan
@ -152,11 +160,12 @@ func TestHtlcSuccessSingleStage(t *testing.T) {
{ {
// We send a confirmation for our sweep tx to indicate // We send a confirmation for our sweep tx to indicate
// that our sweep succeeded. // that our sweep succeeded.
preCheckpoint: func(ctx *htlcSuccessResolverTestContext, preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error { _ bool) error {
// The resolver will create and publish a sweep // The resolver will create and publish a sweep
// tx. // tx.
ctx.resolver.Sweeper.(*mockSweeper). resolver := ctx.resolver.(*htlcSuccessResolver)
resolver.Sweeper.(*mockSweeper).
createSweepTxChan <- sweepTx createSweepTxChan <- sweepTx
// Confirm the sweep, which should resolve it. // Confirm the sweep, which should resolve it.
@ -242,7 +251,7 @@ func TestHtlcSuccessSecondStageResolution(t *testing.T) {
// It will then wait for the Nursery to spend the // It will then wait for the Nursery to spend the
// output. We send a spend notification for our output // output. We send a spend notification for our output
// to resolve our htlc. // to resolve our htlc.
preCheckpoint: func(ctx *htlcSuccessResolverTestContext, preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error { _ bool) error {
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
SpendingTx: sweepTx, SpendingTx: sweepTx,
@ -361,11 +370,11 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
{ {
// The HTLC output on the commitment should be offered // The HTLC output on the commitment should be offered
// to the sweeper. We'll notify that it gets spent. // to the sweeper. We'll notify that it gets spent.
preCheckpoint: func(ctx *htlcSuccessResolverTestContext, preCheckpoint: func(ctx *htlcResolverTestContext,
_ bool) error { _ bool) error {
inp := <-ctx.resolver.Sweeper.(*mockSweeper). resolver := ctx.resolver.(*htlcSuccessResolver)
sweptInputs inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
op := inp.OutPoint() op := inp.OutPoint()
if *op != commitOutpoint { if *op != commitOutpoint {
return fmt.Errorf("outpoint %v swept, "+ return fmt.Errorf("outpoint %v swept, "+
@ -389,7 +398,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
{ {
// The resolver will wait for the second-level's CSV // The resolver will wait for the second-level's CSV
// lock to expire. // lock to expire.
preCheckpoint: func(ctx *htlcSuccessResolverTestContext, preCheckpoint: func(ctx *htlcResolverTestContext,
resumed bool) error { resumed bool) error {
// If we are resuming from a checkpoint, we // If we are resuming from a checkpoint, we
@ -410,8 +419,8 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
// We expect it to sweep the second-level // We expect it to sweep the second-level
// transaction we notfied about above. // transaction we notfied about above.
inp := <-ctx.resolver.Sweeper.(*mockSweeper). resolver := ctx.resolver.(*htlcSuccessResolver)
sweptInputs inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
op := inp.OutPoint() op := inp.OutPoint()
exp := wire.OutPoint{ exp := wire.OutPoint{
Hash: reSignedHash, Hash: reSignedHash,
@ -451,7 +460,7 @@ type checkpoint struct {
// preCheckpoint is a method that will be called before we reach the // preCheckpoint is a method that will be called before we reach the
// checkpoint, to carry out any needed operations to drive the resolver // checkpoint, to carry out any needed operations to drive the resolver
// in this stage. // in this stage.
preCheckpoint func(*htlcSuccessResolverTestContext, bool) error preCheckpoint func(*htlcResolverTestContext, bool) error
// data we expect the resolver to be checkpointed with next. // data we expect the resolver to be checkpointed with next.
incubating bool incubating bool
@ -471,8 +480,15 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution,
// We first run the resolver from start to finish, ensuring it gets // We first run the resolver from start to finish, ensuring it gets
// checkpointed at every expected stage. We store the checkpointed data // checkpointed at every expected stage. We store the checkpointed data
// for the next portion of the test. // for the next portion of the test.
ctx := newHtlcSuccessResolverTextContext(t, nil) ctx := newHtlcResolverTestContext(t,
ctx.resolver.htlcResolution = resolution func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
return &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlc: htlc,
htlcResolution: resolution,
}
},
)
checkpointedState := runFromCheckpoint(t, ctx, checkpoints) checkpointedState := runFromCheckpoint(t, ctx, checkpoints)
@ -480,8 +496,18 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution,
// run the test from that checkpoint. // run the test from that checkpoint.
for i := range checkpointedState { for i := range checkpointedState {
cp := bytes.NewReader(checkpointedState[i]) cp := bytes.NewReader(checkpointedState[i])
ctx := newHtlcSuccessResolverTextContext(t, cp) ctx := newHtlcResolverTestContext(t,
ctx.resolver.htlcResolution = resolution func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
resolver, err := newSuccessResolverFromReader(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. // Run from the given checkpoint, ensuring we'll hit the rest.
_ = runFromCheckpoint(t, ctx, checkpoints[i+1:]) _ = runFromCheckpoint(t, ctx, checkpoints[i+1:])
@ -490,7 +516,7 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution,
// runFromCheckpoint executes the Resolve method on the success resolver, and // runFromCheckpoint executes the Resolve method on the success resolver, and
// asserts that it checkpoints itself according to the expected checkpoints. // asserts that it checkpoints itself according to the expected checkpoints.
func runFromCheckpoint(t *testing.T, ctx *htlcSuccessResolverTestContext, func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext,
expectedCheckpoints []checkpoint) [][]byte { expectedCheckpoints []checkpoint) [][]byte {
defer timeout(t)() defer timeout(t)()
@ -501,25 +527,34 @@ func runFromCheckpoint(t *testing.T, ctx *htlcSuccessResolverTestContext,
// checkpointed state and reports are equal to what we expect. // checkpointed state and reports are equal to what we expect.
nextCheckpoint := 0 nextCheckpoint := 0
checkpointChan := make(chan struct{}) checkpointChan := make(chan struct{})
ctx.resolver.Checkpoint = func(resolver ContractResolver, ctx.checkpoint = func(resolver ContractResolver,
reports ...*channeldb.ResolverReport) error { reports ...*channeldb.ResolverReport) error {
if nextCheckpoint >= len(expectedCheckpoints) { if nextCheckpoint >= len(expectedCheckpoints) {
t.Fatal("did not expect more checkpoints") t.Fatal("did not expect more checkpoints")
} }
h := resolver.(*htlcSuccessResolver) var resolved, incubating bool
cp := expectedCheckpoints[nextCheckpoint] if h, ok := resolver.(*htlcSuccessResolver); ok {
resolved = h.resolved
if h.resolved != cp.resolved { incubating = h.outputIncubating
t.Fatalf("expected checkpoint to be resolve=%v, had %v", }
cp.resolved, h.resolved) if h, ok := resolver.(*htlcTimeoutResolver); ok {
resolved = h.resolved
incubating = h.outputIncubating
} }
if !reflect.DeepEqual(h.outputIncubating, cp.incubating) { cp := expectedCheckpoints[nextCheckpoint]
if resolved != cp.resolved {
t.Fatalf("expected checkpoint to be resolve=%v, had %v",
cp.resolved, resolved)
}
if !reflect.DeepEqual(incubating, cp.incubating) {
t.Fatalf("expected checkpoint to be have "+ t.Fatalf("expected checkpoint to be have "+
"incubating=%v, had %v", cp.incubating, "incubating=%v, had %v", cp.incubating,
h.outputIncubating) incubating)
} }

@ -8,6 +8,7 @@ import (
"testing" "testing"
"time" "time"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
@ -481,3 +482,834 @@ func TestHtlcTimeoutResolver(t *testing.T) {
} }
} }
} }
// 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:])
}
}