contractcourt: persist timed out incoming htlc resolver reports
Incoming htlcs that are timed out or failed (invalid htlc or invoice condition not met), save a single on chain resolution because we don't need to take any actions on them ourselves (we don't need to worry about 2 stage claims since this is the success path for our peer).
This commit is contained in:
parent
f5b20b7429
commit
cf739f3f87
@ -49,6 +49,9 @@ type ResolverType uint8
|
|||||||
const (
|
const (
|
||||||
// ResolverTypeAnchor represents a resolver for an anchor output.
|
// ResolverTypeAnchor represents a resolver for an anchor output.
|
||||||
ResolverTypeAnchor ResolverType = 0
|
ResolverTypeAnchor ResolverType = 0
|
||||||
|
|
||||||
|
// ResolverTypeIncomingHtlc represents resolution of an incoming htlc.
|
||||||
|
ResolverTypeIncomingHtlc ResolverType = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResolverOutcome indicates the outcome for the resolver that that the contract
|
// ResolverOutcome indicates the outcome for the resolver that that the contract
|
||||||
@ -64,6 +67,16 @@ const (
|
|||||||
// chain. This may be the case for anchors that we did not sweep, or
|
// chain. This may be the case for anchors that we did not sweep, or
|
||||||
// outputs that were not economical to sweep.
|
// outputs that were not economical to sweep.
|
||||||
ResolverOutcomeUnclaimed ResolverOutcome = 1
|
ResolverOutcomeUnclaimed ResolverOutcome = 1
|
||||||
|
|
||||||
|
// ResolverOutcomeAbandoned indicates that we did not attempt to claim
|
||||||
|
// an output on chain. This is the case for htlcs that we could not
|
||||||
|
// decode to claim, or invoice which we fail when an attempt is made
|
||||||
|
// to settle them on chain.
|
||||||
|
ResolverOutcomeAbandoned ResolverOutcome = 2
|
||||||
|
|
||||||
|
// ResolverOutcomeTimeout indicates that a contract was timed out on
|
||||||
|
// chain.
|
||||||
|
ResolverOutcomeTimeout ResolverOutcome = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResolverReport provides an account of the outcome of a resolver. This differs
|
// ResolverReport provides an account of the outcome of a resolver. This differs
|
||||||
|
@ -81,7 +81,14 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
// present itself when we crash before processRemoteAdds in the
|
// present itself when we crash before processRemoteAdds in the
|
||||||
// link has ran.
|
// link has ran.
|
||||||
h.resolved = true
|
h.resolved = true
|
||||||
return nil, nil
|
|
||||||
|
// We write a report to disk that indicates we could not decode
|
||||||
|
// the htlc.
|
||||||
|
resReport := h.report().resolverReport(
|
||||||
|
nil, channeldb.ResolverTypeIncomingHtlc,
|
||||||
|
channeldb.ResolverOutcomeAbandoned,
|
||||||
|
)
|
||||||
|
return nil, h.PutResolverReport(nil, resReport)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register for block epochs. After registration, the current height
|
// Register for block epochs. After registration, the current height
|
||||||
@ -120,7 +127,14 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
"abandoning", h, h.htlcResolution.ClaimOutpoint,
|
"abandoning", h, h.htlcResolution.ClaimOutpoint,
|
||||||
h.htlcExpiry, currentHeight)
|
h.htlcExpiry, currentHeight)
|
||||||
h.resolved = true
|
h.resolved = true
|
||||||
return nil, h.Checkpoint(h)
|
|
||||||
|
// Finally, get our report and checkpoint our resolver with a
|
||||||
|
// timeout outcome report.
|
||||||
|
report := h.report().resolverReport(
|
||||||
|
nil, channeldb.ResolverTypeIncomingHtlc,
|
||||||
|
channeldb.ResolverOutcomeTimeout,
|
||||||
|
)
|
||||||
|
return nil, h.Checkpoint(h, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
// applyPreimage is a helper function that will populate our internal
|
// applyPreimage is a helper function that will populate our internal
|
||||||
@ -158,16 +172,6 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the HTLC hasn't expired yet, then we may still be able to claim
|
|
||||||
// it if we learn of the pre-image, so we'll subscribe to the preimage
|
|
||||||
// database to see if it turns up, or the HTLC times out.
|
|
||||||
//
|
|
||||||
// NOTE: This is done BEFORE opportunistically querying the db, to
|
|
||||||
// ensure the preimage can't be delivered between querying and
|
|
||||||
// registering for the preimage subscription.
|
|
||||||
preimageSubscription := h.PreimageDB.SubscribeUpdates()
|
|
||||||
defer preimageSubscription.CancelSubscription()
|
|
||||||
|
|
||||||
// Define a closure to process htlc resolutions either directly or
|
// Define a closure to process htlc resolutions either directly or
|
||||||
// triggered by future notifications.
|
// triggered by future notifications.
|
||||||
processHtlcResolution := func(e invoices.HtlcResolution) (
|
processHtlcResolution := func(e invoices.HtlcResolution) (
|
||||||
@ -196,7 +200,14 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
h.htlcExpiry, currentHeight)
|
h.htlcExpiry, currentHeight)
|
||||||
|
|
||||||
h.resolved = true
|
h.resolved = true
|
||||||
return nil, h.Checkpoint(h)
|
|
||||||
|
// Checkpoint our resolver with an abandoned outcome
|
||||||
|
// because we take no further action on this htlc.
|
||||||
|
report := h.report().resolverReport(
|
||||||
|
nil, channeldb.ResolverTypeIncomingHtlc,
|
||||||
|
channeldb.ResolverOutcomeAbandoned,
|
||||||
|
)
|
||||||
|
return nil, h.Checkpoint(h, report)
|
||||||
|
|
||||||
// Error if the resolution type is unknown, we are only
|
// Error if the resolution type is unknown, we are only
|
||||||
// expecting settles and fails.
|
// expecting settles and fails.
|
||||||
@ -206,71 +217,91 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a buffered hodl chan to prevent deadlock.
|
var (
|
||||||
hodlChan := make(chan interface{}, 1)
|
hodlChan chan interface{}
|
||||||
|
witnessUpdates <-chan lntypes.Preimage
|
||||||
// Notify registry that we are potentially resolving as an exit hop
|
|
||||||
// on-chain. If this HTLC indeed pays to an existing invoice, the
|
|
||||||
// invoice registry will tell us what to do with the HTLC. This is
|
|
||||||
// identical to HTLC resolution in the link.
|
|
||||||
circuitKey := channeldb.CircuitKey{
|
|
||||||
ChanID: h.ShortChanID,
|
|
||||||
HtlcID: h.htlc.HtlcIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
resolution, err := h.Registry.NotifyExitHopHtlc(
|
|
||||||
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
|
||||||
circuitKey, hodlChan, payload,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if payload.FwdInfo.NextHop == hop.Exit {
|
||||||
return nil, err
|
// Create a buffered hodl chan to prevent deadlock.
|
||||||
}
|
hodlChan = make(chan interface{}, 1)
|
||||||
|
|
||||||
defer h.Registry.HodlUnsubscribeAll(hodlChan)
|
// Notify registry that we are potentially resolving as an exit
|
||||||
|
// hop on-chain. If this HTLC indeed pays to an existing
|
||||||
// Take action based on the resolution we received. If the htlc was
|
// invoice, the invoice registry will tell us what to do with
|
||||||
// settled, or a htlc for a known invoice failed we can resolve it
|
// the HTLC. This is identical to HTLC resolution in the link.
|
||||||
// directly. If the resolution is nil, the htlc was neither accepted
|
circuitKey := channeldb.CircuitKey{
|
||||||
// nor failed, so we cannot take action yet.
|
ChanID: h.ShortChanID,
|
||||||
switch res := resolution.(type) {
|
HtlcID: h.htlc.HtlcIndex,
|
||||||
case *invoices.HtlcFailResolution:
|
|
||||||
// In the case where the htlc failed, but the invoice was known
|
|
||||||
// to the registry, we can directly resolve the htlc.
|
|
||||||
if res.Outcome != invoices.ResultInvoiceNotFound {
|
|
||||||
return processHtlcResolution(resolution)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we settled the htlc, we can resolve it.
|
resolution, err := h.Registry.NotifyExitHopHtlc(
|
||||||
case *invoices.HtlcSettleResolution:
|
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
||||||
return processHtlcResolution(resolution)
|
circuitKey, hodlChan, payload,
|
||||||
|
)
|
||||||
// If the resolution is nil, the htlc was neither settled nor failed so
|
if err != nil {
|
||||||
// we cannot take action at present.
|
|
||||||
case nil:
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown htlc resolution type: %T",
|
|
||||||
resolution)
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the epochs and preimage subscriptions initialized, we'll query
|
|
||||||
// to see if we already know the preimage.
|
|
||||||
preimage, ok := h.PreimageDB.LookupPreimage(h.htlc.RHash)
|
|
||||||
if ok {
|
|
||||||
// If we do, then this means we can claim the HTLC! However,
|
|
||||||
// we don't know how to ourselves, so we'll return our inner
|
|
||||||
// resolver which has the knowledge to do so.
|
|
||||||
if err := applyPreimage(preimage); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &h.htlcSuccessResolver, nil
|
defer h.Registry.HodlUnsubscribeAll(hodlChan)
|
||||||
|
|
||||||
|
// Take action based on the resolution we received. If the htlc
|
||||||
|
// was settled, or a htlc for a known invoice failed we can
|
||||||
|
// resolve it directly. If the resolution is nil, the htlc was
|
||||||
|
// neither accepted nor failed, so we cannot take action yet.
|
||||||
|
switch res := resolution.(type) {
|
||||||
|
case *invoices.HtlcFailResolution:
|
||||||
|
// In the case where the htlc failed, but the invoice
|
||||||
|
// was known to the registry, we can directly resolve
|
||||||
|
// the htlc.
|
||||||
|
if res.Outcome != invoices.ResultInvoiceNotFound {
|
||||||
|
return processHtlcResolution(resolution)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we settled the htlc, we can resolve it.
|
||||||
|
case *invoices.HtlcSettleResolution:
|
||||||
|
return processHtlcResolution(resolution)
|
||||||
|
|
||||||
|
// If the resolution is nil, the htlc was neither settled nor
|
||||||
|
// failed so we cannot take action at present.
|
||||||
|
case nil:
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown htlc resolution type: %T",
|
||||||
|
resolution)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the HTLC hasn't expired yet, then we may still be able to
|
||||||
|
// claim it if we learn of the pre-image, so we'll subscribe to
|
||||||
|
// the preimage database to see if it turns up, or the HTLC
|
||||||
|
// times out.
|
||||||
|
//
|
||||||
|
// NOTE: This is done BEFORE opportunistically querying the db,
|
||||||
|
// to ensure the preimage can't be delivered between querying
|
||||||
|
// and registering for the preimage subscription.
|
||||||
|
preimageSubscription := h.PreimageDB.SubscribeUpdates()
|
||||||
|
defer preimageSubscription.CancelSubscription()
|
||||||
|
|
||||||
|
// With the epochs and preimage subscriptions initialized, we'll
|
||||||
|
// query to see if we already know the preimage.
|
||||||
|
preimage, ok := h.PreimageDB.LookupPreimage(h.htlc.RHash)
|
||||||
|
if ok {
|
||||||
|
// If we do, then this means we can claim the HTLC!
|
||||||
|
// However, we don't know how to ourselves, so we'll
|
||||||
|
// return our inner resolver which has the knowledge to
|
||||||
|
// do so.
|
||||||
|
if err := applyPreimage(preimage); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &h.htlcSuccessResolver, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
witnessUpdates = preimageSubscription.WitnessUpdates
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case preimage := <-preimageSubscription.WitnessUpdates:
|
case preimage := <-witnessUpdates:
|
||||||
// We received a new preimage, but we need to ignore
|
// We received a new preimage, but we need to ignore
|
||||||
// all except the preimage we are waiting for.
|
// all except the preimage we are waiting for.
|
||||||
if !preimage.Matches(h.htlc.RHash) {
|
if !preimage.Matches(h.htlc.RHash) {
|
||||||
@ -305,7 +336,13 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
h.htlcResolution.ClaimOutpoint,
|
h.htlcResolution.ClaimOutpoint,
|
||||||
h.htlcExpiry, currentHeight)
|
h.htlcExpiry, currentHeight)
|
||||||
h.resolved = true
|
h.resolved = true
|
||||||
return nil, h.Checkpoint(h)
|
|
||||||
|
report := h.report().resolverReport(
|
||||||
|
nil,
|
||||||
|
channeldb.ResolverTypeIncomingHtlc,
|
||||||
|
channeldb.ResolverOutcomeTimeout,
|
||||||
|
)
|
||||||
|
return nil, h.Checkpoint(h, report)
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-h.quit:
|
case <-h.quit:
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
||||||
@ -13,6 +14,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/invoices"
|
"github.com/lightningnetwork/lnd/invoices"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -26,6 +28,7 @@ var (
|
|||||||
testResCircuitKey = channeldb.CircuitKey{}
|
testResCircuitKey = channeldb.CircuitKey{}
|
||||||
testOnionBlob = []byte{4, 5, 6}
|
testOnionBlob = []byte{4, 5, 6}
|
||||||
testAcceptHeight int32 = 1234
|
testAcceptHeight int32 = 1234
|
||||||
|
testHtlcAmount = 2300
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestHtlcIncomingResolverFwdPreimageKnown tests resolution of a forwarded htlc
|
// TestHtlcIncomingResolverFwdPreimageKnown tests resolution of a forwarded htlc
|
||||||
@ -34,11 +37,7 @@ func TestHtlcIncomingResolverFwdPreimageKnown(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t, false)
|
||||||
ctx.registry.notifyResolution = invoices.NewFailResolution(
|
|
||||||
testResCircuitKey, testHtlcExpiry,
|
|
||||||
invoices.ResultInvoiceNotFound,
|
|
||||||
)
|
|
||||||
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
ctx.waitForResult(true)
|
ctx.waitForResult(true)
|
||||||
@ -51,11 +50,7 @@ func TestHtlcIncomingResolverFwdContestedSuccess(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t, false)
|
||||||
ctx.registry.notifyResolution = invoices.NewFailResolution(
|
|
||||||
testResCircuitKey, testHtlcExpiry,
|
|
||||||
invoices.ResultInvoiceNotFound,
|
|
||||||
)
|
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
|
|
||||||
// Simulate a new block coming in. HTLC is not yet expired.
|
// Simulate a new block coming in. HTLC is not yet expired.
|
||||||
@ -71,16 +66,36 @@ func TestHtlcIncomingResolverFwdContestedTimeout(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t, false)
|
||||||
ctx.registry.notifyResolution = invoices.NewFailResolution(
|
|
||||||
testResCircuitKey, testHtlcExpiry,
|
// Replace our checkpoint with one which will push reports into a
|
||||||
invoices.ResultInvoiceNotFound,
|
// 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.resolve()
|
||||||
|
|
||||||
// Simulate a new block coming in. HTLC expires.
|
// Simulate a new block coming in. HTLC expires.
|
||||||
ctx.notifyEpoch(testHtlcExpiry)
|
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)
|
ctx.waitForResult(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,11 +105,7 @@ func TestHtlcIncomingResolverFwdTimeout(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t, true)
|
||||||
ctx.registry.notifyResolution = invoices.NewFailResolution(
|
|
||||||
testResCircuitKey, testHtlcExpiry,
|
|
||||||
invoices.ResultInvoiceNotFound,
|
|
||||||
)
|
|
||||||
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
ctx.witnessBeacon.lookupPreimage[testResHash] = testResPreimage
|
||||||
ctx.resolver.htlcExpiry = 90
|
ctx.resolver.htlcExpiry = 90
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
@ -107,7 +118,7 @@ func TestHtlcIncomingResolverExitSettle(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t, true)
|
||||||
ctx.registry.notifyResolution = invoices.NewSettleResolution(
|
ctx.registry.notifyResolution = invoices.NewSettleResolution(
|
||||||
testResPreimage, testResCircuitKey, testAcceptHeight,
|
testResPreimage, testResCircuitKey, testAcceptHeight,
|
||||||
invoices.ResultReplayToSettled,
|
invoices.ResultReplayToSettled,
|
||||||
@ -138,7 +149,7 @@ func TestHtlcIncomingResolverExitCancel(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t, true)
|
||||||
ctx.registry.notifyResolution = invoices.NewFailResolution(
|
ctx.registry.notifyResolution = invoices.NewFailResolution(
|
||||||
testResCircuitKey, testAcceptHeight,
|
testResCircuitKey, testAcceptHeight,
|
||||||
invoices.ResultInvoiceAlreadyCanceled,
|
invoices.ResultInvoiceAlreadyCanceled,
|
||||||
@ -154,7 +165,7 @@ func TestHtlcIncomingResolverExitSettleHodl(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
ctx := newIncomingResolverTestContext(t, true)
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
|
|
||||||
notifyData := <-ctx.registry.notifyChan
|
notifyData := <-ctx.registry.notifyChan
|
||||||
@ -172,9 +183,34 @@ func TestHtlcIncomingResolverExitTimeoutHodl(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
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.resolve()
|
||||||
ctx.notifyEpoch(testHtlcExpiry)
|
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)
|
ctx.waitForResult(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,25 +220,62 @@ func TestHtlcIncomingResolverExitCancelHodl(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newIncomingResolverTestContext(t)
|
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.resolve()
|
||||||
notifyData := <-ctx.registry.notifyChan
|
notifyData := <-ctx.registry.notifyChan
|
||||||
notifyData.hodlChan <- invoices.NewFailResolution(
|
notifyData.hodlChan <- invoices.NewFailResolution(
|
||||||
testResCircuitKey, testAcceptHeight, invoices.ResultCanceled,
|
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)
|
ctx.waitForResult(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockHopIterator struct {
|
type mockHopIterator struct {
|
||||||
|
isExit bool
|
||||||
hop.Iterator
|
hop.Iterator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *mockHopIterator) HopPayload() (*hop.Payload, error) {
|
func (h *mockHopIterator) HopPayload() (*hop.Payload, error) {
|
||||||
return nil, nil
|
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{},
|
||||||
|
}), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockOnionProcessor struct {
|
type mockOnionProcessor struct {
|
||||||
|
isExit bool
|
||||||
offeredOnionBlob []byte
|
offeredOnionBlob []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,7 +288,7 @@ func (o *mockOnionProcessor) ReconstructHopIterator(r io.Reader, rHash []byte) (
|
|||||||
}
|
}
|
||||||
o.offeredOnionBlob = data
|
o.offeredOnionBlob = data
|
||||||
|
|
||||||
return &mockHopIterator{}, nil
|
return &mockHopIterator{isExit: o.isExit}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type incomingResolverTestContext struct {
|
type incomingResolverTestContext struct {
|
||||||
@ -229,7 +302,7 @@ type incomingResolverTestContext struct {
|
|||||||
t *testing.T
|
t *testing.T
|
||||||
}
|
}
|
||||||
|
|
||||||
func newIncomingResolverTestContext(t *testing.T) *incomingResolverTestContext {
|
func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolverTestContext {
|
||||||
notifier := &mockNotifier{
|
notifier := &mockNotifier{
|
||||||
epochChan: make(chan *chainntnfs.BlockEpoch),
|
epochChan: make(chan *chainntnfs.BlockEpoch),
|
||||||
spendChan: make(chan *chainntnfs.SpendDetail),
|
spendChan: make(chan *chainntnfs.SpendDetail),
|
||||||
@ -240,7 +313,7 @@ func newIncomingResolverTestContext(t *testing.T) *incomingResolverTestContext {
|
|||||||
notifyChan: make(chan notifyExitHopData, 1),
|
notifyChan: make(chan notifyExitHopData, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
onionProcessor := &mockOnionProcessor{}
|
onionProcessor := &mockOnionProcessor{isExit: isExit}
|
||||||
|
|
||||||
checkPointChan := make(chan struct{}, 1)
|
checkPointChan := make(chan struct{}, 1)
|
||||||
|
|
||||||
@ -272,6 +345,7 @@ func newIncomingResolverTestContext(t *testing.T) *incomingResolverTestContext {
|
|||||||
contractResolverKit: *newContractResolverKit(cfg),
|
contractResolverKit: *newContractResolverKit(cfg),
|
||||||
htlcResolution: lnwallet.IncomingHtlcResolution{},
|
htlcResolution: lnwallet.IncomingHtlcResolution{},
|
||||||
htlc: channeldb.HTLC{
|
htlc: channeldb.HTLC{
|
||||||
|
Amt: lnwire.MilliSatoshi(testHtlcAmount),
|
||||||
RHash: testResHash,
|
RHash: testResHash,
|
||||||
OnionBlob: testOnionBlob,
|
OnionBlob: testOnionBlob,
|
||||||
},
|
},
|
||||||
|
Loading…
Reference in New Issue
Block a user