lnd/contractcourt/htlc_incoming_contest_resolver_test.go
Carla Kirk-Cohen 776c889267
multi: return route role from HopPayload
We need to know what role we're playing to be able to handle errors
correctly, but the information that we need for this is held by our
iterator:
- Whether we had a blinding point in update add (blinding kit)
- Whether we had a blinding point in payload

As we're now going to use the route role return value even when our
err!=nil, we rename the error to signal that we're using less
canonical golang here.

An alternative to this approach is to attach a RouteRole to our
ErrInvalidPayload. The downside of that approach is:
- Propagate context through parsing (whether we had updateAddHtlc)
- Clumsy handling for errors that are not of type ErrInvalidPayload
2024-04-25 09:46:31 -04:00

447 lines
12 KiB
Go

package contractcourt
import (
"bytes"
"io"
"testing"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnmock"
"github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
const (
testInitialBlockHeight = 100
testHtlcExpiry = 150
)
var (
testResPreimage = lntypes.Preimage{1, 2, 3}
testResHash = testResPreimage.Hash()
testResCircuitKey = models.CircuitKey{}
testAcceptHeight int32 = 1234
testHtlcAmount = 2300
)
// TestHtlcIncomingResolverFwdPreimageKnown tests resolution of a forwarded htlc
// for which the preimage is already known initially.
func TestHtlcIncomingResolverFwdPreimageKnown(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, false)
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
ctx.resolve()
ctx.waitForResult(true)
}
// TestHtlcIncomingResolverFwdContestedSuccess tests resolution of a forwarded
// htlc for which the preimage becomes known after the resolver has been
// started.
func TestHtlcIncomingResolverFwdContestedSuccess(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, false)
ctx.resolve()
// Simulate a new block coming in. HTLC is not yet expired.
ctx.notifyEpoch(testInitialBlockHeight + 1)
ctx.witnessBeacon.preImageUpdates <- testResPreimage
ctx.waitForResult(true)
}
// TestHtlcIncomingResolverFwdContestedTimeout tests resolution of a forwarded
// htlc that times out after the resolver has been started.
func TestHtlcIncomingResolverFwdContestedTimeout(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, false)
// Replace our checkpoint with one which will push reports into a
// channel for us to consume. We replace this function on the resolver
// itself because it is created by the test context.
reportChan := make(chan *channeldb.ResolverReport)
ctx.resolver.Checkpoint = func(_ ContractResolver,
reports ...*channeldb.ResolverReport) error {
// Send all of our reports into the channel.
for _, report := range reports {
reportChan <- report
}
return nil
}
ctx.resolve()
// Simulate a new block coming in. HTLC expires.
ctx.notifyEpoch(testHtlcExpiry)
// Assert that we have a failure resolution because our invoice was
// cancelled.
assertResolverReport(t, reportChan, &channeldb.ResolverReport{
Amount: lnwire.MilliSatoshi(testHtlcAmount).ToSatoshis(),
ResolverType: channeldb.ResolverTypeIncomingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
})
ctx.waitForResult(false)
}
// TestHtlcIncomingResolverFwdTimeout tests resolution of a forwarded htlc that
// has already expired when the resolver starts.
func TestHtlcIncomingResolverFwdTimeout(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, true)
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
ctx.resolver.htlcExpiry = 90
ctx.resolve()
ctx.waitForResult(false)
}
// TestHtlcIncomingResolverExitSettle tests resolution of an exit hop htlc for
// which the invoice has already been settled when the resolver starts.
func TestHtlcIncomingResolverExitSettle(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, true)
ctx.registry.notifyResolution = invoices.NewSettleResolution(
testResPreimage, testResCircuitKey, testAcceptHeight,
invoices.ResultReplayToSettled,
)
ctx.resolve()
data := <-ctx.registry.notifyChan
if data.expiry != testHtlcExpiry {
t.Fatal("incorrect expiry")
}
if data.currentHeight != testInitialBlockHeight {
t.Fatal("incorrect block height")
}
ctx.waitForResult(true)
expetedOnion := lnmock.MockOnion()
if !bytes.Equal(
ctx.onionProcessor.offeredOnionBlob, expetedOnion[:],
) {
t.Fatal("unexpected onion blob")
}
}
// TestHtlcIncomingResolverExitCancel tests resolution of an exit hop htlc for
// an invoice that is already canceled when the resolver starts.
func TestHtlcIncomingResolverExitCancel(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, true)
ctx.registry.notifyResolution = invoices.NewFailResolution(
testResCircuitKey, testAcceptHeight,
invoices.ResultInvoiceAlreadyCanceled,
)
ctx.resolve()
ctx.waitForResult(false)
}
// TestHtlcIncomingResolverExitSettleHodl tests resolution of an exit hop htlc
// for a hodl invoice that is settled after the resolver has started.
func TestHtlcIncomingResolverExitSettleHodl(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, true)
ctx.resolve()
notifyData := <-ctx.registry.notifyChan
notifyData.hodlChan <- invoices.NewSettleResolution(
testResPreimage, testResCircuitKey, testAcceptHeight,
invoices.ResultSettled,
)
ctx.waitForResult(true)
}
// TestHtlcIncomingResolverExitTimeoutHodl tests resolution of an exit hop htlc
// for a hodl invoice that times out.
func TestHtlcIncomingResolverExitTimeoutHodl(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, true)
// Replace our checkpoint with one which will push reports into a
// channel for us to consume. We replace this function on the resolver
// itself because it is created by the test context.
reportChan := make(chan *channeldb.ResolverReport)
ctx.resolver.Checkpoint = func(_ ContractResolver,
reports ...*channeldb.ResolverReport) error {
// Send all of our reports into the channel.
for _, report := range reports {
reportChan <- report
}
return nil
}
ctx.resolve()
ctx.notifyEpoch(testHtlcExpiry)
// Assert that we have a failure resolution because our invoice was
// cancelled.
assertResolverReport(t, reportChan, &channeldb.ResolverReport{
Amount: lnwire.MilliSatoshi(testHtlcAmount).ToSatoshis(),
ResolverType: channeldb.ResolverTypeIncomingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
})
ctx.waitForResult(false)
}
// TestHtlcIncomingResolverExitCancelHodl tests resolution of an exit hop htlc
// for a hodl invoice that is canceled after the resolver has started.
func TestHtlcIncomingResolverExitCancelHodl(t *testing.T) {
t.Parallel()
defer timeout()()
ctx := newIncomingResolverTestContext(t, true)
// Replace our checkpoint with one which will push reports into a
// channel for us to consume. We replace this function on the resolver
// itself because it is created by the test context.
reportChan := make(chan *channeldb.ResolverReport)
ctx.resolver.Checkpoint = func(_ ContractResolver,
reports ...*channeldb.ResolverReport) error {
// Send all of our reports into the channel.
for _, report := range reports {
reportChan <- report
}
return nil
}
ctx.resolve()
notifyData := <-ctx.registry.notifyChan
notifyData.hodlChan <- invoices.NewFailResolution(
testResCircuitKey, testAcceptHeight, invoices.ResultCanceled,
)
// Assert that we have a failure resolution because our invoice was
// cancelled.
assertResolverReport(t, reportChan, &channeldb.ResolverReport{
Amount: lnwire.MilliSatoshi(testHtlcAmount).ToSatoshis(),
ResolverType: channeldb.ResolverTypeIncomingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeAbandoned,
})
ctx.waitForResult(false)
}
type mockHopIterator struct {
isExit bool
hop.Iterator
}
func (h *mockHopIterator) HopPayload() (*hop.Payload, hop.RouteRole, error) {
var nextAddress [8]byte
if !h.isExit {
nextAddress = [8]byte{0x01}
}
return hop.NewLegacyPayload(&sphinx.HopData{
Realm: [1]byte{},
NextAddress: nextAddress,
ForwardAmount: 100,
OutgoingCltv: 40,
ExtraBytes: [12]byte{},
}), hop.RouteRoleCleartext, nil
}
func (h *mockHopIterator) EncodeNextHop(w io.Writer) error {
return nil
}
type mockOnionProcessor struct {
isExit bool
offeredOnionBlob []byte
}
func (o *mockOnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte,
_ hop.ReconstructBlindingInfo) (hop.Iterator, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
o.offeredOnionBlob = data
return &mockHopIterator{isExit: o.isExit}, nil
}
type incomingResolverTestContext struct {
registry *mockRegistry
witnessBeacon *mockWitnessBeacon
resolver *htlcIncomingContestResolver
notifier *mock.ChainNotifier
onionProcessor *mockOnionProcessor
resolveErr chan error
nextResolver ContractResolver
finalHtlcOutcomeStored bool
t *testing.T
}
func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolverTestContext {
notifier := &mock.ChainNotifier{
EpochChan: make(chan *chainntnfs.BlockEpoch),
SpendChan: make(chan *chainntnfs.SpendDetail),
ConfChan: make(chan *chainntnfs.TxConfirmation),
}
witnessBeacon := newMockWitnessBeacon()
registry := &mockRegistry{
notifyChan: make(chan notifyExitHopData, 1),
}
onionProcessor := &mockOnionProcessor{isExit: isExit}
checkPointChan := make(chan struct{}, 1)
c := &incomingResolverTestContext{
registry: registry,
witnessBeacon: witnessBeacon,
notifier: notifier,
onionProcessor: onionProcessor,
t: t,
}
htlcNotifier := &mockHTLCNotifier{}
chainCfg := ChannelArbitratorConfig{
ChainArbitratorConfig: ChainArbitratorConfig{
Notifier: notifier,
PreimageDB: witnessBeacon,
Registry: registry,
OnionProcessor: onionProcessor,
PutFinalHtlcOutcome: func(chanId lnwire.ShortChannelID,
htlcId uint64, settled bool) error {
c.finalHtlcOutcomeStored = true
return nil
},
HtlcNotifier: htlcNotifier,
Budget: *DefaultBudgetConfig(),
QueryIncomingCircuit: func(
circuit models.CircuitKey) *models.CircuitKey {
return nil
},
},
PutResolverReport: func(_ kvdb.RwTx,
_ *channeldb.ResolverReport) error {
return nil
},
}
cfg := ResolverConfig{
ChannelArbitratorConfig: chainCfg,
Checkpoint: func(_ ContractResolver,
_ ...*channeldb.ResolverReport) error {
checkPointChan <- struct{}{}
return nil
},
}
c.resolver = &htlcIncomingContestResolver{
htlcSuccessResolver: &htlcSuccessResolver{
contractResolverKit: *newContractResolverKit(cfg),
htlcResolution: lnwallet.IncomingHtlcResolution{},
htlc: channeldb.HTLC{
Amt: lnwire.MilliSatoshi(testHtlcAmount),
RHash: testResHash,
OnionBlob: lnmock.MockOnion(),
},
},
htlcExpiry: testHtlcExpiry,
}
return c
}
func (i *incomingResolverTestContext) resolve() {
// Start resolver.
i.resolveErr = make(chan error, 1)
go func() {
var err error
i.nextResolver, err = i.resolver.Resolve(false)
i.resolveErr <- err
}()
// Notify initial block height.
i.notifyEpoch(testInitialBlockHeight)
}
func (i *incomingResolverTestContext) notifyEpoch(height int32) {
i.notifier.EpochChan <- &chainntnfs.BlockEpoch{
Height: height,
}
}
func (i *incomingResolverTestContext) waitForResult(expectSuccessRes bool) {
i.t.Helper()
err := <-i.resolveErr
if err != nil {
i.t.Fatal(err)
}
if !expectSuccessRes {
if i.nextResolver != nil {
i.t.Fatal("expected no next resolver")
}
require.True(i.t, i.finalHtlcOutcomeStored,
"expected final htlc outcome to be stored")
return
}
successResolver, ok := i.nextResolver.(*htlcSuccessResolver)
if !ok {
i.t.Fatal("expected htlcSuccessResolver")
}
if successResolver.htlcResolution.Preimage != testResPreimage {
i.t.Fatal("invalid preimage")
}
successTx := successResolver.htlcResolution.SignedSuccessTx
if successTx != nil &&
!bytes.Equal(successTx.TxIn[0].Witness[3], testResPreimage[:]) {
i.t.Fatal("invalid preimage")
}
}